[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\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: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: ['https://www.paypal.com/paypalme/abedelazizshehadeh1/USD5']\n"
  },
  {
    "path": ".gitignore",
    "content": ".classpath\n.DS_Store\n.externalNativeBuild\n.project\n.gradle\n.mtj.tmp\n.vscode\n.settings\n.cxx\n\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n\nlocal.properties\nmaven-repository\nmvn-clone\nbuild\ncaptures\ngen\nout\ntarget\ntmpmob\n\n*.class\n*.txt\n*.ear\n*.iml\n*.jar\n*.keystore\n*.log\n*.nar\n*.rar\n*.tar.gz\n*.war\n*.zip"
  },
  {
    "path": ".idea/.name",
    "content": "VideoCompressor"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <JetCodeStyleSettings>\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </JetCodeStyleSettings>\n    <codeStyleSettings language=\"XML\">\n      <indentOptions>\n        <option name=\"CONTINUATION_INDENT_SIZE\" value=\"4\" />\n      </indentOptions>\n      <arrangement>\n        <rules>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:android</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>xmlns:.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:id</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*:name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>name</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>style</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>^$</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>ANDROID_ATTRIBUTE_ORDER</order>\n            </rule>\n          </section>\n          <section>\n            <rule>\n              <match>\n                <AND>\n                  <NAME>.*</NAME>\n                  <XML_ATTRIBUTE />\n                  <XML_NAMESPACE>.*</XML_NAMESPACE>\n                </AND>\n              </match>\n              <order>BY_NAME</order>\n            </rule>\n          </section>\n        </rules>\n      </arrangement>\n    </codeStyleSettings>\n    <codeStyleSettings language=\"kotlin\">\n      <option name=\"CODE_STYLE_DEFAULTS\" value=\"KOTLIN_OFFICIAL\" />\n    </codeStyleSettings>\n  </code_scheme>\n</component>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "content": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n  </state>\n</component>"
  },
  {
    "path": ".idea/compiler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <bytecodeTargetLevel target=\"17\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/dictionaries/abdsh.xml",
    "content": "<component name=\"ProjectDictionaryState\">\n  <dictionary name=\"abdsh\">\n    <words>\n      <w>ftyp</w>\n      <w>mdat</w>\n      <w>moov</w>\n      <w>muxer</w>\n    </words>\n  </dictionary>\n</component>"
  },
  {
    "path": ".idea/gradle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersion=\"1\" />\n  <component name=\"GradleSettings\">\n    <option name=\"linkedExternalProjectsSettings\">\n      <GradleProjectSettings>\n        <option name=\"externalProjectPath\" value=\"$PROJECT_DIR$\" />\n        <option name=\"gradleJvm\" value=\"#GRADLE_LOCAL_JAVA_HOME\" />\n        <option name=\"modules\">\n          <set>\n            <option value=\"$PROJECT_DIR$\" />\n            <option value=\"$PROJECT_DIR$/app\" />\n            <option value=\"$PROJECT_DIR$/lightcompressor\" />\n          </set>\n        </option>\n        <option name=\"resolveExternalAnnotations\" value=\"false\" />\n      </GradleProjectSettings>\n    </option>\n  </component>\n</project>"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"JavaDoc\" enabled=\"true\" level=\"WARNING\" enabled_by_default=\"true\">\n      <option name=\"TOP_LEVEL_CLASS_OPTIONS\">\n        <value>\n          <option name=\"ACCESS_JAVADOC_REQUIRED_FOR\" value=\"none\" />\n          <option name=\"REQUIRED_TAGS\" value=\"\" />\n        </value>\n      </option>\n      <option name=\"INNER_CLASS_OPTIONS\">\n        <value>\n          <option name=\"ACCESS_JAVADOC_REQUIRED_FOR\" value=\"none\" />\n          <option name=\"REQUIRED_TAGS\" value=\"\" />\n        </value>\n      </option>\n      <option name=\"METHOD_OPTIONS\">\n        <value>\n          <option name=\"ACCESS_JAVADOC_REQUIRED_FOR\" value=\"none\" />\n          <option name=\"REQUIRED_TAGS\" value=\"@return@param@throws or @exception\" />\n        </value>\n      </option>\n      <option name=\"FIELD_OPTIONS\">\n        <value>\n          <option name=\"ACCESS_JAVADOC_REQUIRED_FOR\" value=\"none\" />\n          <option name=\"REQUIRED_TAGS\" value=\"\" />\n        </value>\n      </option>\n      <option name=\"IGNORE_DEPRECATED\" value=\"false\" />\n      <option name=\"IGNORE_JAVADOC_PERIOD\" value=\"true\" />\n      <option name=\"IGNORE_DUPLICATED_THROWS\" value=\"false\" />\n      <option name=\"IGNORE_POINT_TO_ITSELF\" value=\"false\" />\n      <option name=\"myAdditionalJavadocTags\" value=\"date\" />\n    </inspection_tool>\n  </profile>\n</component>"
  },
  {
    "path": ".idea/jarRepositories.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RemoteRepositoriesConfiguration\">\n    <remote-repository>\n      <option name=\"id\" value=\"central\" />\n      <option name=\"name\" value=\"Maven Central repository\" />\n      <option name=\"url\" value=\"https://repo1.maven.org/maven2\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"jboss.community\" />\n      <option name=\"name\" value=\"JBoss Community repository\" />\n      <option name=\"url\" value=\"https://repository.jboss.org/nexus/content/repositories/public/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"BintrayJCenter\" />\n      <option name=\"name\" value=\"BintrayJCenter\" />\n      <option name=\"url\" value=\"https://jcenter.bintray.com/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"maven\" />\n      <option name=\"name\" value=\"maven\" />\n      <option name=\"url\" value=\"https://jitpack.io\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"Google\" />\n      <option name=\"name\" value=\"Google\" />\n      <option name=\"url\" value=\"https://dl.google.com/dl/android/maven2/\" />\n    </remote-repository>\n    <remote-repository>\n      <option name=\"id\" value=\"MavenRepo\" />\n      <option name=\"name\" value=\"MavenRepo\" />\n      <option name=\"url\" value=\"https://repo.maven.apache.org/maven2/\" />\n    </remote-repository>\n  </component>\n</project>"
  },
  {
    "path": ".idea/kotlinc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinJpsPluginSettings\">\n    <option name=\"version\" value=\"1.8.21\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/misc.xml",
    "content": "<project version=\"4\">\n  <component name=\"CMakeSettings\">\n    <configurations>\n      <configuration PROFILE_NAME=\"Debug\" CONFIG_NAME=\"Debug\" />\n    </configurations>\n  </component>\n  <component name=\"DesignSurface\">\n    <option name=\"filePathToZoomLevelMap\">\n      <map>\n        <entry key=\"..\\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/activity_main.xml\" value=\"0.17831813576494426\" />\n        <entry key=\"..\\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/activity_video_player.xml\" value=\"0.3641304347826087\" />\n        <entry key=\"..\\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/content_main.xml\" value=\"0.3641304347826087\" />\n        <entry key=\"..\\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/recycler_view_item.xml\" value=\"0.3641304347826087\" />\n      </map>\n    </option>\n  </component>\n  <component name=\"ProjectRootManager\" version=\"2\" languageLevel=\"JDK_17\" project-jdk-name=\"jbr-17\" project-jdk-type=\"JavaSDK\">\n    <output url=\"file://$PROJECT_DIR$/build/classes\" />\n  </component>\n  <component name=\"ProjectType\">\n    <option name=\"id\" value=\"Android\" />\n  </component>\n</project>"
  },
  {
    "path": ".idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [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": "README.md",
    "content": "[![JitPack](https://jitpack.io/v/AbedElazizShe/LightCompressor.svg)](https://jitpack.io/#AbedElazizShe/LightCompressor)\n\n\n# LightCompressor\n\nLightCompressor can now be used in Flutter through [light_compressor](https://pub.dev/packages/light_compressor) plugin.\n\nA powerful and easy-to-use video compression library for android uses [MediaCodec](https://developer.android.com/reference/android/media/MediaCodec) API. This library generates a compressed MP4 video with a modified width, height, and bitrate (the number of bits per\nseconds that determines the video and audio files’ size and quality). It is based on Telegram for Android project.\n\nThe general idea of how the library works is that, extreme high bitrate is reduced while maintaining a good video quality resulting in a smaller size.\n\nI would like to mention that the set attributes for size and quality worked just great in my projects and met the expectations. It may or may not meet yours. I’d appreciate your feedback so I can enhance the compression process.\n\n**LightCompressor is now available in iOS**, have a look at [LightCompressor_iOS](https://github.com/AbedElazizShe/LightCompressor_iOS).\n\n# Change Logs\n\n## What's new in 1.3.3\n\n- Thanks to [LiewJunTung](https://github.com/AbedElazizShe/LightCompressor/pull/181) for improving the error handling.\n- Thanks to [CristianMG](https://github.com/AbedElazizShe/LightCompressor/pull/182) for improving the storage configuration and making the library testable.\n- Thanks to [dan3988](https://github.com/AbedElazizShe/LightCompressor/pull/188) for replacing video size with resizer which made using the library way more flexible.\n- Thanks to [imSzukala](https://github.com/AbedElazizShe/LightCompressor/pull/191) for changing min supported api to 21.\n- Thanks to [josebraz](https://github.com/AbedElazizShe/LightCompressor/pull/192) for improving codec profile approach.\n- Thanks to [ryccoatika](https://github.com/AbedElazizShe/LightCompressor/pull/198) for improving exception handling for the coroutines.\n\n\n## How it works\nWhen the video file is called to be compressed, the library checks if the user wants to set a min bitrate to avoid compressing low resolution videos. This becomes handy if you don’t want the video to be compressed every time it is to be processed to avoid having very bad quality after multiple rounds of compression. The minimum is;\n* Bitrate: 2mbps\n\nYou can as well pass custom resizer and videoBitrate values if you don't want the library to auto-generate the values for you.\n\nThese values were tested on a huge set of videos and worked fine and fast with them. They might be changed based on the project needs and expectations.\n\n## Demo\n![Demo](/pictures/demo.gif)\n\nUsage\n--------\nTo use this library, you must add the following permission to allow read and write to external storage. Refer to the sample app for a reference on how to start compression with the right setup.\n\n**API < 29**\n\n```xml\n<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n<uses-permission\n    android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n    android:maxSdkVersion=\"28\"\n    tools:ignore=\"ScopedStorage\" />\n```\n\n**API >= 29**\n\n```xml\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"\n    android:maxSdkVersion=\"32\"/>\n```\n\n**API >= 33**\n\n```xml\n <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\"/>\n```\n\n```kotlin\n\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n     // request READ_MEDIA_VIDEO run-time permission\n } else {\n     // request WRITE_EXTERNAL_STORAGE run-time permission\n }\n```\n\nAnd import the following dependencies to use kotlin coroutines\n\n### Groovy\n\n```groovy\nimplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.coroutines}\"\nimplementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.coroutines}\"\n```\n\nThen just call [VideoCompressor.start()] and pass **context**, **uris**, **isStreamable**, **configureWith**, and either **sharedStorageConfiguration OR appSpecificStorageConfiguration**.\n\nThe method has a callback for 5 functions;\n1) OnStart - called when compression started\n2) OnSuccess - called when compression completed with no errors/exceptions\n3) OnFailure - called when an exception occurred or video bitrate and size are below the minimum required for compression.\n4) OnProgress - called with progress new value\n5) OnCancelled - called when the job is cancelled\n\n### Important Notes:\n\n- All the callback functions returns an index for the video being compressed in the same order of the urls passed to the library. You can use this index to update the UI\nor retrieve information about the original uri/file.\n- The source video must be provided as a list of content uris.\n- OnSuccess returns the path of the stored video.\n- If you want an output video that is optimised to be streamed, ensure you pass [isStreamable] flag is true.\n\n### Configuration values\n\n- VideoQuality: VERY_HIGH (original-bitrate * 0.6) , HIGH (original-bitrate * 0.4), MEDIUM (original-bitrate * 0.3), LOW (original-bitrate * 0.2), OR VERY_LOW (original-bitrate * 0.1)\n\n- isMinBitrateCheckEnabled: this means, don't compress if bitrate is less than 2mbps\n\n- videoBitrateInMbps: any custom bitrate value in Mbps.\n\n- disableAudio: true/false to generate a video without audio. False by default.\n\n- resizer: Function to resize the video dimensions. `VideoResizer.auto` by default.\n\n\n## The StorageConfiguration is an interface which indicate library where will be saved the File\n\n#### Library provides some behaviors defined to be more easy to use, specified the next\n\n### AppSpecificStorageConfiguration Configuration values\n\n- subFolderName: a subfolder name created in app's specific storage. \n\n### SharedStorageConfiguration Configuration values\n\n- saveAt: the directory where the video should be saved in. Must be one of the following; [SaveLocation.pictures], [SaveLocation.movies], or [SaveLocation.downloads].\n- subFolderName: a subfolder name created in shared storage. \n\n### CacheStorageConfiguration\n- There are no configuration values create a file in cache directory as Google defined, to get more info go to [here](https://developer.android.com/training/data-storage/app-specific?hl=es-419)\n\n### Fully custom configuration\n- If any of these behaviors fit with your needs, you can create your own StorageConfiguration, just implement the interface and pass it to the library\n\n```kotlin\nclass FullyCustomizedStorageConfiguration(\n) : StorageConfiguration {\n    override fun createFileToSave(\n        context: Context,\n        videoFile: File,\n        fileName: String,\n        shouldSave: Boolean\n    ): File = ??? What you need \n}\n\n```\n\nTo cancel the compression job, just call [VideoCompressor.cancel()]\n\n### Kotlin\n\n```kotlin\nVideoCompressor.start(\n   context = applicationContext, // => This is required\n   uris = List<Uri>, // => Source can be provided as content uris\n   isStreamable = false, \n   // THIS STORAGE\n   storageConfiguration = SharedStorageConfiguration(\n       saveAt = SaveLocation.movies, // => default is movies\n       subFolderName = \"my-videos\" // => optional\n   )\n   configureWith = Configuration(\n      videoNames = listOf<String>(), /*list of video names, the size should be similar to the passed uris*/\n      quality = VideoQuality.MEDIUM,\n      isMinBitrateCheckEnabled = true,\n      videoBitrateInMbps = 5, /*Int, ignore, or null*/\n      disableAudio = false, /*Boolean, or ignore*/\n      resizer = VideoResizer.matchSize(360, 480) /*VideoResizer, ignore, or null*/\n   ),\n   listener = object : CompressionListener {\n       override fun onProgress(index: Int, percent: Float) {\n          // Update UI with progress value\n          runOnUiThread {\n          }\n       }\n\n       override fun onStart(index: Int) {\n          // Compression start\n       }\n\n       override fun onSuccess(index: Int, size: Long, path: String?) {\n         // On Compression success\n       }\n\n       override fun onFailure(index: Int, failureMessage: String) {\n         // On Failure\n       }\n\n       override fun onCancelled(index: Int) {\n         // On Cancelled\n       }\n\n   }\n)\n```\n\n## Common issues\n\n- Sending the video to whatsapp when disableAudio = false, won't succeed [ at least for now ]. Whatsapp's own compression does not work with\nLightCompressor library. You can send the video as document.\n\n- You cannot call Toast.makeText() and other functions dealing with the UI directly in onProgress() which is a worker thread. They need to be called\nfrom within the main thread. Have a look at the example code above for more information.\n\n## Reporting issues\nTo report an issue, please specify the following:\n- Device name\n- Android version\n\n## Compatibility\nMinimum Android SDK: LightCompressor requires a minimum API level of 21.\n\n## How to add to your project?\n#### Gradle\n\nEnsure Kotlin version is `1.8.21`\n\nInclude this in your Project-level build.gradle file:\n\n### Groovy\n\n```groovy\nallprojects {\n    repositories {\n        .\n        .\n        .\n        maven { url 'https://jitpack.io' }\n    }\n}\n```\n\nInclude this in your Module-level build.gradle file:\n\n### Groovy\n\n```groovy\nimplementation 'com.github.AbedElazizShe:LightCompressor:1.3.3'\n```\n\nIf you're facing problems with the setup, edit settings.gradle by adding this at the beginning of the file:\n\n```\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n    }\n}\n```\n\n## Getting help\nFor questions, suggestions, or anything else, email elaziz.shehadeh(at)gmail.com\n\n## Credits\n[Telegram](https://github.com/DrKLO/Telegram) for Android.\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-kapt'\n\nandroid {\n    compileSdkVersion 33\n    defaultConfig {\n        applicationId \"com.abedelazizshe.lightcompressor\"\n        minSdkVersion 21\n        targetSdkVersion 33\n        versionCode 1\n        versionName \"1.0.0\"\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_1_8\n        targetCompatibility = JavaVersion.VERSION_1_8\n    }\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_1_8\n    }\n\n    buildFeatures {\n        viewBinding true\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(':lightcompressor')\n\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib:1.8.21\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4\"\n\n    implementation 'androidx.appcompat:appcompat:1.6.1'\n    implementation 'androidx.core:core-ktx:1.10.1'\n    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'\n    implementation \"com.google.android.material:material:1.9.0\"\n    implementation \"com.github.bumptech.glide:glide:4.12.0\"\n    kapt 'com.github.bumptech.glide:compiler:4.12.0'\n    implementation 'com.google.android.exoplayer:exoplayer:2.16.1'\n    implementation 'androidx.recyclerview:recyclerview:1.3.0'\n    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'\n\n    testImplementation \"junit:junit:4.13.2\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.5\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.5.1\"\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/abedelazizshe/lightcompressor/ExampleInstrumentedTest.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.abedelazizshe.lightcompressor\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.abedelazizshe.lightcompressor\">\n\n    <queries>\n        <intent>\n            <action android:name=\"android.media.action.VIDEO_CAPTURE\" />\n        </intent>\n    </queries>\n\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"32\"/>\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\"\n        tools:ignore=\"ScopedStorage\" />\n    <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\"/>\n\n    <uses-feature\n        android:name=\"android.hardware.camera\"\n        android:required=\"true\" />\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        tools:ignore=\"AllowBackup,GoogleAppIndexingWarning\"\n        >\n\n        <activity android:name=\".VideoPlayerActivity\" />\n        <activity\n            android:name=\".MainActivity\"\n            android:theme=\"@style/AppTheme.NoActionBar\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <property\n            android:name=\"android.content.MEDIA_CAPABILITIES\"\n            android:resource=\"@xml/media_capabilities\" />\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.ClipData\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.provider.MediaStore\nimport android.util.Log\nimport android.view.View\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.app.ActivityCompat\nimport androidx.core.content.ContextCompat\nimport androidx.lifecycle.lifecycleScope\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.abedelazizshe.lightcompressor.databinding.ActivityMainBinding\nimport com.abedelazizshe.lightcompressorlibrary.CompressionListener\nimport com.abedelazizshe.lightcompressorlibrary.VideoCompressor\nimport com.abedelazizshe.lightcompressorlibrary.VideoQuality\nimport com.abedelazizshe.lightcompressorlibrary.config.Configuration\nimport com.abedelazizshe.lightcompressorlibrary.config.VideoResizer\nimport com.abedelazizshe.lightcompressorlibrary.config.SaveLocation\nimport com.abedelazizshe.lightcompressorlibrary.config.SharedStorageConfiguration\nimport kotlinx.coroutines.launch\n\n/**\n * Created by AbedElaziz Shehadeh on 26 Jan, 2020\n * elaziz.shehadeh@gmail.com\n */\nclass MainActivity : AppCompatActivity() {\n\n    private lateinit var binding: ActivityMainBinding\n\n    companion object {\n        const val REQUEST_SELECT_VIDEO = 0\n        const val REQUEST_CAPTURE_VIDEO = 1\n    }\n\n    private val uris = mutableListOf<Uri>()\n    private val data = mutableListOf<VideoDetailsModel>()\n    private lateinit var adapter: RecyclerViewAdapter\n\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        binding = ActivityMainBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        setReadStoragePermission()\n\n        binding.pickVideo.setOnClickListener {\n            pickVideo()\n        }\n\n        binding.recordVideo.setOnClickListener {\n            dispatchTakeVideoIntent()\n        }\n\n        binding.cancel.setOnClickListener {\n            VideoCompressor.cancel()\n        }\n\n        val recyclerview = findViewById<RecyclerView>(R.id.recyclerview)\n        recyclerview.layoutManager = LinearLayoutManager(this)\n        adapter = RecyclerViewAdapter(applicationContext, data)\n        recyclerview.adapter = adapter\n    }\n\n    //Pick a video file from device\n    private fun pickVideo() {\n        val intent = Intent()\n        intent.apply {\n            type = \"video/*\"\n            action = Intent.ACTION_PICK\n        }\n        intent.putExtra(\n            Intent.EXTRA_ALLOW_MULTIPLE,\n            true\n        )\n        startActivityForResult(Intent.createChooser(intent, \"Select video\"), REQUEST_SELECT_VIDEO)\n    }\n\n    private fun dispatchTakeVideoIntent() {\n        Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->\n            takeVideoIntent.resolveActivity(packageManager)?.also {\n                startActivityForResult(takeVideoIntent, REQUEST_CAPTURE_VIDEO)\n            }\n        }\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {\n\n        reset()\n\n        if (resultCode == Activity.RESULT_OK)\n            if (requestCode == REQUEST_SELECT_VIDEO || requestCode == REQUEST_CAPTURE_VIDEO) {\n                handleResult(intent)\n            }\n\n        super.onActivityResult(requestCode, resultCode, intent)\n    }\n\n    private fun handleResult(data: Intent?) {\n        val clipData: ClipData? = data?.clipData\n        if (clipData != null) {\n            for (i in 0 until clipData.itemCount) {\n                val videoItem = clipData.getItemAt(i)\n                uris.add(videoItem.uri)\n            }\n            processVideo()\n        } else if (data != null && data.data != null) {\n            val uri = data.data\n            uris.add(uri!!)\n            processVideo()\n        }\n    }\n\n    private fun reset() {\n        uris.clear()\n        binding.mainContents.visibility = View.GONE\n        data.clear()\n        adapter.notifyDataSetChanged()\n    }\n\n    private fun setReadStoragePermission() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            if (ContextCompat.checkSelfPermission(\n                    this,\n                    Manifest.permission.READ_MEDIA_VIDEO,\n                ) != PackageManager.PERMISSION_GRANTED\n            ) {\n\n                if (!ActivityCompat.shouldShowRequestPermissionRationale(\n                        this,\n                        Manifest.permission.READ_MEDIA_VIDEO\n                    )\n                ) {\n                    ActivityCompat.requestPermissions(\n                        this,\n                        arrayOf(Manifest.permission.READ_MEDIA_VIDEO),\n                        1\n                    )\n                }\n            }\n        } else {\n            if (ContextCompat.checkSelfPermission(\n                    this,\n                    Manifest.permission.WRITE_EXTERNAL_STORAGE,\n                ) != PackageManager.PERMISSION_GRANTED\n            ) {\n\n                if (!ActivityCompat.shouldShowRequestPermissionRationale(\n                        this,\n                        Manifest.permission.WRITE_EXTERNAL_STORAGE\n                    )\n                ) {\n                    ActivityCompat.requestPermissions(\n                        this,\n                        arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),\n                        1\n                    )\n                }\n            }\n        }\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun processVideo() {\n        binding.mainContents.visibility = View.VISIBLE\n\n        lifecycleScope.launch {\n            VideoCompressor.start(\n                context = applicationContext,\n                uris,\n                isStreamable = false,\n                storageConfiguration = SharedStorageConfiguration(\n                    saveAt = SaveLocation.movies,\n                    subFolderName = \"my-demo-videos\"\n                ),\n                configureWith = Configuration(\n                    quality = VideoQuality.LOW,\n                    videoNames = uris.map { uri -> uri.pathSegments.last() },\n                    isMinBitrateCheckEnabled = false,\n                    resizer = VideoResizer.limitSize(1280.0)\n                ),\n                listener = object : CompressionListener {\n                    override fun onProgress(index: Int, percent: Float) {\n                        //Update UI\n                        if (percent <= 100)\n                            runOnUiThread {\n                                data[index] = VideoDetailsModel(\n                                    \"\",\n                                    uris[index],\n                                    \"\",\n                                    percent\n                                )\n                                adapter.notifyDataSetChanged()\n                            }\n                    }\n\n                    override fun onStart(index: Int) {\n                        data.add(\n                            index,\n                            VideoDetailsModel(\"\", uris[index], \"\")\n                        )\n                        runOnUiThread {\n                            adapter.notifyDataSetChanged()\n                        }\n\n                    }\n\n                    override fun onSuccess(index: Int, size: Long, path: String?) {\n                        data[index] = VideoDetailsModel(\n                            path,\n                            uris[index],\n                            getFileSize(size),\n                            100F\n                        )\n                        runOnUiThread {\n                            adapter.notifyDataSetChanged()\n                        }\n                    }\n\n                    override fun onFailure(index: Int, failureMessage: String) {\n                        Log.wtf(\"failureMessage\", failureMessage)\n                    }\n\n                    override fun onCancelled(index: Int) {\n                        Log.wtf(\"TAG\", \"compression has been cancelled\")\n                        // make UI changes, cleanup, etc\n                    }\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.ProgressBar\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\nimport com.bumptech.glide.Glide\n\nclass RecyclerViewAdapter(private val context: Context, private val list: List<VideoDetailsModel>) :\n    RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {\n        val view = LayoutInflater.from(parent.context)\n            .inflate(R.layout.recycler_view_item, parent, false)\n\n        return ViewHolder(view)\n    }\n\n    override fun onBindViewHolder(holder: ViewHolder, position: Int) {\n\n        val itemsViewModel = list[position]\n        val newSize = \"Size after compression: ${itemsViewModel.newSize}\"\n        val progress = \"${itemsViewModel.progress.toLong()}%\"\n\n        if (itemsViewModel.progress > 0 && itemsViewModel.progress < 100) {\n            holder.progress.visibility = View.VISIBLE\n            holder.progress.text = progress\n\n            holder.progressBar.visibility = View.VISIBLE\n            holder.progressBar.progress = itemsViewModel.progress.toInt()\n        } else {\n            holder.progress.visibility = View.GONE\n            holder.progressBar.visibility = View.GONE\n        }\n\n        if (itemsViewModel.newSize.isNotBlank()) {\n            holder.newSize.text = newSize\n            holder.newSize.visibility = View.VISIBLE\n        } else {\n            holder.newSize.visibility = View.GONE\n        }\n\n        Glide.with(context).load(itemsViewModel.uri).into(holder.videoImage)\n\n        holder.itemView.setOnClickListener {\n            VideoPlayerActivity.start(\n                it.context,\n                itemsViewModel.playableVideoPath\n            )\n        }\n    }\n\n    override fun getItemCount(): Int {\n        return list.size\n    }\n\n    class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {\n        val videoImage: ImageView = itemView.findViewById(R.id.videoImage)\n        val newSize: TextView = itemView.findViewById(R.id.newSize)\n        val progress: TextView = itemView.findViewById(R.id.progress)\n        val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/Utils.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.net.Uri\nimport android.provider.MediaStore\nimport java.io.*\nimport java.text.DecimalFormat\nimport kotlin.math.log10\nimport kotlin.math.pow\n\nfun getMediaPath(context: Context, uri: Uri): String {\n\n    val resolver = context.contentResolver\n    val projection = arrayOf(MediaStore.Video.Media.DATA)\n    var cursor: Cursor? = null\n    try {\n        cursor = resolver.query(uri, projection, null, null, null)\n        return if (cursor != null) {\n            val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)\n            cursor.moveToFirst()\n            cursor.getString(columnIndex)\n\n        } else \"\"\n\n    } catch (e: Exception) {\n        resolver.let {\n            val filePath = (context.applicationInfo.dataDir + File.separator\n                    + System.currentTimeMillis())\n            val file = File(filePath)\n\n            resolver.openInputStream(uri)?.use { inputStream ->\n                FileOutputStream(file).use { outputStream ->\n                    val buf = ByteArray(4096)\n                    var len: Int\n                    while (inputStream.read(buf).also { len = it } > 0) outputStream.write(\n                        buf,\n                        0,\n                        len\n                    )\n                }\n            }\n            return file.absolutePath\n        }\n    } finally {\n        cursor?.close()\n    }\n}\n\nfun getFileSize(size: Long): String {\n    if (size <= 0)\n        return \"0\"\n\n    val units = arrayOf(\"B\", \"KB\", \"MB\", \"GB\", \"TB\")\n    val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt()\n\n    return DecimalFormat(\"#,##0.#\").format(\n        size / 1024.0.pow(digitGroups.toDouble())\n    ) + \" \" + units[digitGroups]\n}\n\n//The following methods can be alternative to [getMediaPath].\n// todo(abed): remove [getPathFromUri], [getVideoExtension], and [copy]\nfun getPathFromUri(context: Context, uri: Uri): String {\n    var file: File? = null\n    var inputStream: InputStream? = null\n    var outputStream: OutputStream? = null\n    var success = false\n    try {\n        val extension: String = getVideoExtension(uri)\n        inputStream = context.contentResolver.openInputStream(uri)\n        file = File.createTempFile(\"compressor\", extension, context.cacheDir)\n        file.deleteOnExit()\n        outputStream = FileOutputStream(file)\n        if (inputStream != null) {\n            copy(inputStream, outputStream)\n            success = true\n        }\n    } catch (ignored: IOException) {\n    } finally {\n        try {\n            inputStream?.close()\n        } catch (ignored: IOException) {\n        }\n        try {\n            outputStream?.close()\n        } catch (ignored: IOException) {\n            // If closing the output stream fails, we cannot be sure that the\n            // target file was written in full. Flushing the stream merely moves\n            // the bytes into the OS, not necessarily to the file.\n            success = false\n        }\n    }\n    return if (success) file!!.path else \"\"\n}\n\n/** @return extension of video with dot, or default .mp4 if it none.\n */\nprivate fun getVideoExtension(uriVideo: Uri): String {\n    var extension: String? = null\n    try {\n        val imagePath = uriVideo.path\n        if (imagePath != null && imagePath.lastIndexOf(\".\") != -1) {\n            extension = imagePath.substring(imagePath.lastIndexOf(\".\") + 1)\n        }\n    } catch (e: Exception) {\n        extension = null\n    }\n    if (extension == null || extension.isEmpty()) {\n        //default extension for matches the previous behavior of the plugin\n        extension = \"mp4\"\n    }\n    return \".$extension\"\n}\n\nprivate fun copy(`in`: InputStream, out: OutputStream) {\n    val buffer = ByteArray(4 * 1024)\n    var bytesRead: Int\n    while (`in`.read(buffer).also { bytesRead = it } != -1) {\n        out.write(buffer, 0, bytesRead)\n    }\n    out.flush()\n}\n"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport android.net.Uri\n\ndata class VideoDetailsModel(\n    val playableVideoPath: String?,\n    val uri: Uri,\n    val newSize: String,\n    val progress: Float = 0F\n)\n"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/VideoPlayerActivity.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport com.abedelazizshe.lightcompressor.databinding.ActivityVideoPlayerBinding\nimport com.google.android.exoplayer2.DefaultLoadControl\nimport com.google.android.exoplayer2.DefaultRenderersFactory\nimport com.google.android.exoplayer2.SimpleExoPlayer\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.Util\nimport java.io.File\n\n/**\n * Created by AbedElaziz Shehadeh on 26 Jan, 2020\n * elaziz.shehadeh@gmail.com\n */\nclass VideoPlayerActivity : AppCompatActivity() {\n\n    private lateinit var binding: ActivityVideoPlayerBinding\n\n    private lateinit var exoPlayer: SimpleExoPlayer\n    private var uri = \"\"\n\n    companion object {\n        fun start(context: Context, uri: String?) {\n            val intent = Intent(context, VideoPlayerActivity::class.java)\n                .putExtra(\"uri\", uri)\n            context.startActivity(intent)\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        binding = ActivityVideoPlayerBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n\n        intent?.extras?.let {\n            uri = it.getString(\"uri\", \"\")\n        }\n        initializePlayer()\n    }\n\n    private fun initializePlayer() {\n\n        val trackSelector = DefaultTrackSelector(this)\n        val loadControl = DefaultLoadControl()\n        val rendererFactory = DefaultRenderersFactory(this)\n\n        exoPlayer = SimpleExoPlayer.Builder(this, rendererFactory)\n            .setLoadControl(loadControl)\n            .setTrackSelector(trackSelector)\n            .build()\n    }\n\n    private fun play(uri: Uri) {\n\n        val userAgent = Util.getUserAgent(this, getString(R.string.app_name))\n        val mediaSource = ProgressiveMediaSource\n            .Factory(DefaultDataSourceFactory(this, userAgent))\n            .createMediaSource(uri)\n\n        binding.epVideoView.player = exoPlayer\n\n        exoPlayer.prepare(mediaSource)\n        exoPlayer.playWhenReady = true\n    }\n\n    override fun onStart() {\n        super.onStart()\n        playVideo()\n    }\n\n    private fun playVideo() {\n        val file = File(uri)\n        val localUri = Uri.fromFile(file)\n        play(localUri)\n    }\n\n    override fun onStop() {\n        super.onStop()\n        exoPlayer.stop()\n        exoPlayer.release()\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#008577\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_white_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:width=\"50dp\"\n\tandroid:height=\"50dp\"\n\tandroid:viewportHeight=\"24.0\"\n\tandroid:viewportWidth=\"24.0\">\n\n\t<path\n\t\tandroid:fillColor=\"#c4ffffff\"\n\t\tandroid:pathData=\"M10,16.5l6,-4.5 -6,-4.5v9zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_video_library_white_24dp.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#FF000000\" android:pathData=\"M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"78.5885\"\n                android:endY=\"90.9159\"\n                android:startX=\"48.7653\"\n                android:startY=\"61.0927\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.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:layout_height=\"match_parent\"\n    tools:context=\".MainActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/appBarLayout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"@style/AppTheme.AppBarOverlay\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\">\n\n        <RelativeLayout\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n\n            <TextView\n                android:id=\"@+id/title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_centerInParent=\"true\"\n                android:text=\"@string/home_title\"\n                android:textColor=\"@color/colorWhite\"\n                android:textSize=\"16sp\" />\n\n            <Button\n                android:id=\"@+id/cancel\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignParentEnd=\"true\"\n                android:layout_centerVertical=\"true\"\n                android:text=\"cancel\" />\n        </RelativeLayout>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n\n    <androidx.constraintlayout.widget.ConstraintLayout\n        android:id=\"@+id/mainContents\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_margin=\"16dp\"\n        android:visibility=\"gone\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n        app:layout_constraintBottom_toTopOf=\"@+id/linearLayout\"\n        app:layout_constraintTop_toBottomOf=\"@+id/appBarLayout\"\n        tools:layout_editor_absoluteX=\"16dp\">\n\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/recyclerview\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:layout_constraintBottom_toBottomOf=\"parent\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\" />\n\n\n    </androidx.constraintlayout.widget.ConstraintLayout>\n\n    <LinearLayout\n        android:id=\"@+id/linearLayout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\"\n        android:orientation=\"horizontal\"\n        android:padding=\"16dp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\">\n\n        <androidx.appcompat.widget.AppCompatButton\n            android:id=\"@+id/pickVideo\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginEnd=\"8dp\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/colorBrown\"\n            android:text=\"@string/pick_video\"\n            android:textColor=\"@color/colorWhite\" />\n\n        <androidx.appcompat.widget.AppCompatButton\n            android:id=\"@+id/recordVideo\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/colorBrown\"\n            android:text=\"@string/record_video\"\n\n            android:textColor=\"@color/colorWhite\" />\n\n    </LinearLayout>\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_video_player.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    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/colorBlack\">\n\n    <com.google.android.exoplayer2.ui.PlayerView\n        android:id=\"@+id/ep_video_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/content_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView 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=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:minWidth=\"120dp\"\n    android:visibility=\"visible\"\n    app:cardCornerRadius=\"5dp\"\n    app:cardElevation=\"0dp\"\n    app:cardMaxElevation=\"0dp\"\n    app:cardPreventCornerOverlap=\"true\"\n    app:cardUseCompatPadding=\"true\">\n\n    <FrameLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\">\n\n        <ImageView\n            android:id=\"@+id/videoImage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"150dp\"\n            android:minWidth=\"150dp\"\n            android:scaleType=\"centerCrop\"\n            tools:background=\"@tools:sample/avatars\" />\n\n        <ImageView\n            android:id=\"@+id/playPause\"\n            android:layout_width=\"42dp\"\n            android:layout_height=\"42dp\"\n            android:layout_gravity=\"center\"\n            android:background=\"@android:color/transparent\"\n            android:src=\"@drawable/ic_play_white_24dp\" />\n\n\n    </FrameLayout>\n\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/recycler_view_item.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:layout_height=\"wrap_content\">\n\n    <include\n        android:id=\"@+id/videoLayout\"\n        layout=\"@layout/content_main\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerHorizontal=\"true\"\n        android:layout_marginTop=\"16dp\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\" />\n\n\n    <TextView\n        android:id=\"@+id/progress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"16dp\"\n        android:layout_marginEnd=\"16dp\"\n        android:gravity=\"center_horizontal\"\n        android:textColor=\"@color/colorPrimary\"\n        android:textSize=\"32sp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/videoLayout\"\n        tools:text=\"Progress\" />\n\n    <ProgressBar\n        android:id=\"@+id/progressBar\"\n        style=\"?android:attr/progressBarStyleHorizontal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/progress\"\n        android:layout_marginStart=\"16dp\"\n        android:layout_marginEnd=\"16dp\"\n        android:max=\"100\"\n        android:progress=\"0\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/progress\" />\n\n    <TextView\n        android:id=\"@+id/newSize\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"16dp\"\n        android:layout_marginTop=\"16dp\"\n        android:layout_marginEnd=\"16dp\"\n        android:textColor=\"@color/colorBlack\"\n        android:textSize=\"16sp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/progressBar\"\n        tools:text=\"Size after compression\" />\n\n    <View\n        android:id=\"@+id/divider\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:layout_marginTop=\"16dp\"\n        android:background=\"?android:attr/listDivider\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/newSize\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#344772</color>\n    <color name=\"colorPrimaryDark\">#002046</color>\n    <color name=\"colorAccent\">#6272a1</color>\n    <color name=\"colorBrown\">#A52A2A</color>\n    <color name=\"colorWhite\">#FFFFFF</color>\n    <color name=\"colorBlack\">#000000</color>\n    <color name=\"colorBackground\">#F5F5F6</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <dimen name=\"fab_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">VideoCompressor</string>\n    <string name=\"home_title\">Video Compressor Sample</string>\n    <string name=\"pick_video\">Pick Video</string>\n    <string name=\"record_video\">Record Video</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.MaterialComponents.Light.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n        <item name=\"android:windowBackground\">@color/colorBackground</item>\n    </style>\n\n    <style name=\"AppTheme.NoActionBar\">\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n    <style name=\"AppTheme.AppBarOverlay\" parent=\"ThemeOverlay.AppCompat.Dark.ActionBar\" />\n\n    <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\" />\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/media_capabilities.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<media-capabilities xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <format android:name=\"HEVC\" supported=\"false\"/>\n    <format android:name=\"HDR10\" supported=\"false\"/>\n    <format android:name=\"HDR10Plus\" supported=\"false\"/>\n    <format android:name=\"Dolby-Vision\" supported=\"false\"/>\n    <format android:name=\"HLG\" supported=\"false\"/>\n    <format android:name=\"SlowMotion\" supported=\"false\"/>\n</media-capabilities>"
  },
  {
    "path": "app/src/test/java/com/abedelazizshe/lightcompressor/ExampleUnitTest.kt",
    "content": "package com.abedelazizshe.lightcompressor\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "build.gradle",
    "content": "buildscript {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.4.2'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21\"\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sat Jan 25 15:17:42 SGT 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip"
  },
  {
    "path": "gradle.properties",
    "content": "android.enableJetifier=true\nandroid.injected.testOnly=false\nandroid.lifecycleProcessor.incremental=true\nandroid.useAndroidX=true\nkapt.include.compile.classpath=false\nkapt.incremental.apt=true\nkapt.verbose=true\nkotlin.code.style=official\norg.gradle.caching=true\norg.gradle.configureondemand=true\norg.gradle.jvmargs=-Xmx1536m\norg.gradle.parallel=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=$(save \"$@\")\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\n# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong\nif [ \"$(uname)\" = \"Darwin\" ] && [ \"$HOME\" = \"$PWD\" ]; then\n  cd \"$(dirname \"$0\")\"\nfi\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\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\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\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\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 Windows variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_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=%*\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": "jitpack.yml",
    "content": "jdk:\n  - openjdk11"
  },
  {
    "path": "lightcompressor/.idea/.gitignore",
    "content": "# Default ignored files\n/workspace.xml"
  },
  {
    "path": "lightcompressor/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'maven-publish'\n\nandroid {\n    compileSdkVersion 33\n    defaultConfig {\n        minSdkVersion 21\n        targetSdkVersion 33\n\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n        }\n    }\n\n    publishing {\n        singleVariant(\"release\") {\n            withSourcesJar()\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib:1.8.21\"\n    implementation \"androidx.core:core-ktx:1.10.1\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4\"\n    implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4\"\n    implementation \"com.googlecode.mp4parser:isoparser:1.0.6\"\n\n    testImplementation \"junit:junit:4.13.2\"\n    androidTestImplementation \"androidx.test.ext:junit:1.1.5\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:3.5.1\"\n}\n\nafterEvaluate {\n    publishing {\n        publications {\n            release(MavenPublication) {\n                from components.release\n                groupId = \"com.github.AbedElazizShe\"\n                artifactId = \"LightCompressor\"\n                version = '1.3.3'\n            }\n        }\n    }\n}"
  },
  {
    "path": "lightcompressor/src/androidTest/java/com/abedelazizshe/lightcompressorlibrary/ExampleInstrumentedTest.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.abedelazizshe.lightcompressorlibrary.test\", appContext.packageName)\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/AndroidManifest.xml",
    "content": "<manifest package=\"com.abedelazizshe.lightcompressorlibrary\" />\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary\n\nimport androidx.annotation.MainThread\nimport androidx.annotation.WorkerThread\n\n/**\n * Created by AbedElaziz Shehadeh on 27 Jan, 2020\n * elaziz.shehadeh@gmail.com\n */\ninterface CompressionListener {\n    @MainThread\n    fun onStart(index: Int)\n\n    @MainThread\n    fun onSuccess(index: Int, size: Long, path: String?)\n\n    @MainThread\n    fun onFailure(index: Int, failureMessage: String)\n\n    @WorkerThread\n    fun onProgress(index: Int, percent: Float)\n\n    @WorkerThread\n    fun onCancelled(index: Int)\n}\n\ninterface CompressionProgressListener {\n    fun onProgressChanged(index: Int, percent: Float)\n    fun onProgressCancelled(index: Int)\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.content.Context.MODE_PRIVATE\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport androidx.annotation.RequiresApi\nimport com.abedelazizshe.lightcompressorlibrary.compressor.Compressor.compressVideo\nimport com.abedelazizshe.lightcompressorlibrary.compressor.Compressor.isRunning\nimport com.abedelazizshe.lightcompressorlibrary.config.*\nimport com.abedelazizshe.lightcompressorlibrary.utils.saveVideoInExternal\nimport com.abedelazizshe.lightcompressorlibrary.video.Result\nimport kotlinx.coroutines.*\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.io.IOException\n\n\nenum class VideoQuality {\n    VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW\n}\n\nobject VideoCompressor : CoroutineScope by MainScope() {\n\n    private var job: Job? = null\n\n    /**\n     * This function compresses a given list of [uris] of video files and writes the compressed\n     * video file at [SharedStorageConfiguration.saveAt] directory, or at [AppSpecificStorageConfiguration.subFolderName]\n     *\n     * The source videos should be provided content uris.\n     *\n     * Only [sharedStorageConfiguration] or [appSpecificStorageConfiguration] must be specified at a\n     * time. Passing both will throw an Exception.\n     *\n     * @param [context] the application context.\n     * @param [uris] the list of content Uris of the video files.\n     * @param [isStreamable] determines if the output video should be prepared for streaming.\n     * @param [sharedStorageConfiguration] configuration for the path directory where the compressed\n     * videos will be saved, and the name of the file\n     * @param [appSpecificStorageConfiguration] configuration for the path directory where the compressed\n     * videos will be saved, the name of the file, and any sub-folders name. The library won't create the subfolder\n     * and will throw an exception if the subfolder does not exist.\n     * @param [listener] a compression listener that listens to compression [CompressionListener.onStart],\n     * [CompressionListener.onProgress], [CompressionListener.onFailure], [CompressionListener.onSuccess]\n     * and if the compression was [CompressionListener.onCancelled]\n     * @param [configureWith] to allow add video compression configuration that could be:\n     * [Configuration.quality] to allow choosing a video quality that can be [VideoQuality.LOW],\n     * [VideoQuality.MEDIUM], [VideoQuality.HIGH], and [VideoQuality.VERY_HIGH].\n     * This defaults to [VideoQuality.MEDIUM]\n     * [Configuration.isMinBitrateCheckEnabled] to determine if the checking for a minimum bitrate threshold\n     * before compression is enabled or not. This default to `true`\n     * [Configuration.videoBitrateInMbps] which is a custom bitrate for the video. You might consider setting\n     * [Configuration.isMinBitrateCheckEnabled] to `false` if your bitrate is less than 2000000.\n     *  * [Configuration.keepOriginalResolution] to keep the original video height and width when compressing.\n     * This defaults to `false`\n     * [Configuration.videoHeight] which is a custom height for the video. Must be specified with [Configuration.videoWidth]\n     * [Configuration.videoWidth] which is a custom width for the video. Must be specified with [Configuration.videoHeight]\n     */\n    @JvmStatic\n    @JvmOverloads\n    fun start(\n        context: Context,\n        uris: List<Uri>,\n        isStreamable: Boolean = false,\n        storageConfiguration: StorageConfiguration,\n        configureWith: Configuration,\n        listener: CompressionListener,\n    ) {\n        // Only one is allowed\n        assert(configureWith.videoNames.size == uris.size)\n\n        doVideoCompression(\n            context,\n            uris,\n            isStreamable,\n            storageConfiguration,\n            configureWith,\n            listener,\n        )\n    }\n\n    /**\n     * Call this function to cancel video compression process which will call [CompressionListener.onCancelled]\n     */\n    @JvmStatic\n    fun cancel() {\n        job?.cancel()\n        isRunning = false\n    }\n\n    private fun doVideoCompression(\n        context: Context,\n        uris: List<Uri>,\n        isStreamable: Boolean,\n        storageConfiguration: StorageConfiguration,\n        configuration: Configuration,\n        listener: CompressionListener,\n    ) {\n        var streamableFile: File? = null\n        for (i in uris.indices) {\n\n            val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->\n                listener.onFailure(i, throwable.message ?: \"\")\n            }\n            val coroutineScope = CoroutineScope(Job() + coroutineExceptionHandler)\n\n            job = coroutineScope.launch(Dispatchers.IO) {\n\n                val job = async { getMediaPath(context, uris[i]) }\n                val path = job.await()\n\n                val desFile = saveVideoFile(\n                    context,\n                    path,\n                    storageConfiguration,\n                    isStreamable,\n                    configuration.videoNames[i],\n                    shouldSave = false\n                )\n\n                if (isStreamable)\n                    streamableFile = saveVideoFile(\n                        context,\n                        path,\n                        storageConfiguration,\n                        null,\n                        configuration.videoNames[i],\n                        shouldSave = false\n                    )\n\n                desFile?.let {\n                    isRunning = true\n                    listener.onStart(i)\n                    val result = startCompression(\n                        i,\n                        context,\n                        uris[i],\n                        desFile.path,\n                        streamableFile?.path,\n                        configuration,\n                        listener,\n                    )\n\n                    // Runs in Main(UI) Thread\n                    if (result.success) {\n                        val savedFile = saveVideoFile(\n                            context,\n                            result.path,\n                            storageConfiguration,\n                            isStreamable,\n                            configuration.videoNames[i],\n                            shouldSave = true\n                        )\n\n                        listener.onSuccess(i, result.size, savedFile?.path)\n                    } else {\n                        listener.onFailure(i, result.failureMessage ?: \"An error has occurred!\")\n                    }\n                }\n            }\n        }\n    }\n\n    private suspend fun startCompression(\n        index: Int,\n        context: Context,\n        srcUri: Uri,\n        destPath: String,\n        streamableFile: String? = null,\n        configuration: Configuration,\n        listener: CompressionListener,\n    ): Result = withContext(Dispatchers.Default) {\n        return@withContext compressVideo(\n            index,\n            context,\n            srcUri,\n            destPath,\n            streamableFile,\n            configuration,\n            object : CompressionProgressListener {\n                override fun onProgressChanged(index: Int, percent: Float) {\n                    listener.onProgress(index, percent)\n                }\n\n                override fun onProgressCancelled(index: Int) {\n                    listener.onCancelled(index)\n                }\n            },\n        )\n    }\n\n    private fun saveVideoFile(\n        context: Context,\n        filePath: String?,\n        storageConfiguration: StorageConfiguration,\n        isStreamable: Boolean?,\n        videoName: String,\n        shouldSave: Boolean\n    ): File? {\n        return filePath?.let {\n            val videoFile = File(filePath)\n            storageConfiguration.createFileToSave(\n                context,\n                videoFile,\n                validatedFileName(\n                    videoName,\n                    isStreamable\n                ),\n                shouldSave\n            )\n        }\n    }\n\n    private fun getMediaPath(context: Context, uri: Uri): String {\n\n        val resolver = context.contentResolver\n        val projection = arrayOf(MediaStore.Video.Media.DATA)\n        var cursor: Cursor? = null\n        try {\n            cursor = resolver.query(uri, projection, null, null, null)\n            return if (cursor != null) {\n                val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)\n                cursor.moveToFirst()\n                cursor.getString(columnIndex)\n\n            } else throw Exception()\n\n        } catch (e: Exception) {\n            resolver.let {\n                val filePath = (context.applicationInfo.dataDir + File.separator\n                        + System.currentTimeMillis())\n                val file = File(filePath)\n\n                resolver.openInputStream(uri)?.use { inputStream ->\n                    FileOutputStream(file).use { outputStream ->\n                        val buf = ByteArray(4096)\n                        var len: Int\n                        while (inputStream.read(buf).also { len = it } > 0) outputStream.write(\n                            buf,\n                            0,\n                            len\n                        )\n                    }\n                }\n                return file.absolutePath\n            }\n        } finally {\n            cursor?.close()\n        }\n    }\n\n    private fun validatedFileName(name: String, isStreamable: Boolean?): String {\n        val videoName = if (isStreamable == null || !isStreamable) name\n        else \"${name}_temp\"\n\n        if (!videoName.contains(\"mp4\")) return \"${videoName}.mp4\"\n        return videoName\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.compressor\n\nimport android.content.Context\nimport android.media.*\nimport android.net.Uri\nimport android.os.Build\nimport android.util.Log\nimport com.abedelazizshe.lightcompressorlibrary.CompressionProgressListener\nimport com.abedelazizshe.lightcompressorlibrary.config.Configuration\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.findTrack\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.getBitrate\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.hasQTI\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.prepareVideoHeight\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.prepareVideoWidth\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.printException\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.setOutputFileParameters\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.setUpMP4Movie\nimport com.abedelazizshe.lightcompressorlibrary.utils.StreamableVideo\nimport com.abedelazizshe.lightcompressorlibrary.utils.roundDimension\nimport com.abedelazizshe.lightcompressorlibrary.video.*\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.nio.ByteBuffer\n\n/**\n * Created by AbedElaziz Shehadeh on 27 Jan, 2020\n * elaziz.shehadeh@gmail.com\n */\nobject Compressor {\n\n    // 2Mbps\n    private const val MIN_BITRATE = 2000000\n\n    // H.264 Advanced Video Coding\n    private const val MIME_TYPE = \"video/avc\"\n    private const val MEDIACODEC_TIMEOUT_DEFAULT = 100L\n\n    private const val INVALID_BITRATE =\n        \"The provided bitrate is smaller than what is needed for compression \" +\n                \"try to set isMinBitRateEnabled to false\"\n\n    var isRunning = true\n\n    suspend fun compressVideo(\n        index: Int,\n        context: Context,\n        srcUri: Uri,\n        destination: String,\n        streamableFile: String?,\n        configuration: Configuration,\n        listener: CompressionProgressListener,\n    ): Result = withContext(Dispatchers.Default) {\n\n        val extractor = MediaExtractor()\n        // Retrieve the source's metadata to be used as input to generate new values for compression\n        val mediaMetadataRetriever = MediaMetadataRetriever()\n\n        try {\n            mediaMetadataRetriever.setDataSource(context, srcUri)\n        } catch (exception: Exception) {\n            printException(exception)\n            return@withContext Result(\n                index,\n                success = false,\n                failureMessage = \"${exception.message}\"\n            )\n        }\n\n        runCatching {\n            extractor.setDataSource(context, srcUri, null)\n        }\n\n        val height: Double = prepareVideoHeight(mediaMetadataRetriever)\n\n        val width: Double = prepareVideoWidth(mediaMetadataRetriever)\n\n        val rotationData =\n            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)\n\n        val bitrateData =\n            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)\n\n        val durationData =\n            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)\n\n\n        if (rotationData.isNullOrEmpty() || bitrateData.isNullOrEmpty() || durationData.isNullOrEmpty()) {\n            // Exit execution\n            return@withContext Result(\n                index,\n                success = false,\n                failureMessage = \"Failed to extract video meta-data, please try again\"\n            )\n        }\n\n        var (rotation, bitrate, duration) = try {\n            Triple(rotationData.toInt(), bitrateData.toInt(), durationData.toLong() * 1000)\n        } catch (e: java.lang.Exception) {\n            return@withContext Result(\n                index,\n                success = false,\n                failureMessage = \"Failed to extract video meta-data, please try again\"\n            )\n        }\n\n        // Check for a min video bitrate before compression\n        // Note: this is an experimental value\n        if (configuration.isMinBitrateCheckEnabled && bitrate <= MIN_BITRATE)\n            return@withContext Result(index, success = false, failureMessage = INVALID_BITRATE)\n\n        //Handle new bitrate value\n        val newBitrate: Int =\n            if (configuration.videoBitrateInMbps == null) getBitrate(bitrate, configuration.quality)\n            else configuration.videoBitrateInMbps!! * 1000000\n\n        //Handle new width and height values\n        val resizer = configuration.resizer\n        val target = resizer?.resize(width, height) ?: Pair(width, height)\n        var newWidth = roundDimension(target.first)\n        var newHeight = roundDimension(target.second)\n\n        //Handle rotation values and swapping height and width if needed\n        rotation = when (rotation) {\n            90, 270 -> {\n                val tempHeight = newHeight\n                newHeight = newWidth\n                newWidth = tempHeight\n                0\n            }\n\n            180 -> 0\n            else -> rotation\n        }\n\n        return@withContext start(\n            index,\n            newWidth,\n            newHeight,\n            destination,\n            newBitrate,\n            streamableFile,\n            configuration.disableAudio,\n            extractor,\n            listener,\n            duration,\n            rotation\n        )\n    }\n\n    @Suppress(\"DEPRECATION\")\n    private fun start(\n        id: Int,\n        newWidth: Int,\n        newHeight: Int,\n        destination: String,\n        newBitrate: Int,\n        streamableFile: String?,\n        disableAudio: Boolean,\n        extractor: MediaExtractor,\n        compressionProgressListener: CompressionProgressListener,\n        duration: Long,\n        rotation: Int\n    ): Result {\n\n        if (newWidth != 0 && newHeight != 0) {\n\n            val cacheFile = File(destination)\n\n            try {\n                // MediaCodec accesses encoder and decoder components and processes the new video\n                //input to generate a compressed/smaller size video\n                val bufferInfo = MediaCodec.BufferInfo()\n\n                // Setup mp4 movie\n                val movie = setUpMP4Movie(rotation, cacheFile)\n\n                // MediaMuxer outputs MP4 in this app\n                val mediaMuxer = MP4Builder().createMovie(movie)\n\n                // Start with video track\n                val videoIndex = findTrack(extractor, isVideo = true)\n\n                extractor.selectTrack(videoIndex)\n                extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)\n                val inputFormat = extractor.getTrackFormat(videoIndex)\n\n                val outputFormat: MediaFormat =\n                    MediaFormat.createVideoFormat(MIME_TYPE, newWidth, newHeight)\n                //set output format\n                setOutputFileParameters(\n                    inputFormat,\n                    outputFormat,\n                    newBitrate,\n                )\n\n                val decoder: MediaCodec\n\n                val hasQTI = hasQTI()\n\n                val encoder = prepareEncoder(outputFormat, hasQTI)\n\n                val inputSurface: InputSurface\n                val outputSurface: OutputSurface\n\n                try {\n                    var inputDone = false\n                    var outputDone = false\n\n                    var videoTrackIndex = -5\n\n                    inputSurface = InputSurface(encoder.createInputSurface())\n                    inputSurface.makeCurrent()\n                    //Move to executing state\n                    encoder.start()\n\n                    outputSurface = OutputSurface()\n\n                    decoder = prepareDecoder(inputFormat, outputSurface)\n\n                    //Move to executing state\n                    decoder.start()\n\n                    while (!outputDone) {\n                        if (!inputDone) {\n\n                            val index = extractor.sampleTrackIndex\n\n                            if (index == videoIndex) {\n                                val inputBufferIndex =\n                                    decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)\n                                if (inputBufferIndex >= 0) {\n                                    val inputBuffer = decoder.getInputBuffer(inputBufferIndex)\n                                    val chunkSize = extractor.readSampleData(inputBuffer!!, 0)\n                                    when {\n                                        chunkSize < 0 -> {\n\n                                            decoder.queueInputBuffer(\n                                                inputBufferIndex,\n                                                0,\n                                                0,\n                                                0L,\n                                                MediaCodec.BUFFER_FLAG_END_OF_STREAM\n                                            )\n                                            inputDone = true\n                                        }\n\n                                        else -> {\n\n                                            decoder.queueInputBuffer(\n                                                inputBufferIndex,\n                                                0,\n                                                chunkSize,\n                                                extractor.sampleTime,\n                                                0\n                                            )\n                                            extractor.advance()\n\n                                        }\n                                    }\n                                }\n\n                            } else if (index == -1) { //end of file\n                                val inputBufferIndex =\n                                    decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)\n                                if (inputBufferIndex >= 0) {\n                                    decoder.queueInputBuffer(\n                                        inputBufferIndex,\n                                        0,\n                                        0,\n                                        0L,\n                                        MediaCodec.BUFFER_FLAG_END_OF_STREAM\n                                    )\n                                    inputDone = true\n                                }\n                            }\n                        }\n\n                        var decoderOutputAvailable = true\n                        var encoderOutputAvailable = true\n\n                        loop@ while (decoderOutputAvailable || encoderOutputAvailable) {\n\n                            if (!isRunning) {\n                                dispose(\n                                    videoIndex,\n                                    decoder,\n                                    encoder,\n                                    inputSurface,\n                                    outputSurface,\n                                    extractor\n                                )\n\n                                compressionProgressListener.onProgressCancelled(id)\n                                return Result(\n                                    id,\n                                    success = false,\n                                    failureMessage = \"The compression has stopped!\"\n                                )\n                            }\n\n                            //Encoder\n                            val encoderStatus =\n                                encoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)\n\n                            when {\n                                encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> encoderOutputAvailable =\n                                    false\n\n                                encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {\n                                    val newFormat = encoder.outputFormat\n                                    if (videoTrackIndex == -5)\n                                        videoTrackIndex = mediaMuxer.addTrack(newFormat, false)\n                                }\n\n                                encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {\n                                    // ignore this status\n                                }\n\n                                encoderStatus < 0 -> throw RuntimeException(\"unexpected result from encoder.dequeueOutputBuffer: $encoderStatus\")\n                                else -> {\n                                    val encodedData = encoder.getOutputBuffer(encoderStatus)\n                                        ?: throw RuntimeException(\"encoderOutputBuffer $encoderStatus was null\")\n\n                                    if (bufferInfo.size > 1) {\n                                        if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {\n                                            mediaMuxer.writeSampleData(\n                                                videoTrackIndex,\n                                                encodedData, bufferInfo, false\n                                            )\n                                        }\n\n                                    }\n\n                                    outputDone =\n                                        bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0\n                                    encoder.releaseOutputBuffer(encoderStatus, false)\n                                }\n                            }\n                            if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) continue@loop\n\n                            //Decoder\n                            val decoderStatus =\n                                decoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)\n                            when {\n                                decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> decoderOutputAvailable =\n                                    false\n\n                                decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {\n                                    // ignore this status\n                                }\n\n                                decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {\n                                    // ignore this status\n                                }\n\n                                decoderStatus < 0 -> throw RuntimeException(\"unexpected result from decoder.dequeueOutputBuffer: $decoderStatus\")\n                                else -> {\n                                    val doRender = bufferInfo.size != 0\n\n                                    decoder.releaseOutputBuffer(decoderStatus, doRender)\n                                    if (doRender) {\n                                        var errorWait = false\n                                        try {\n                                            outputSurface.awaitNewImage()\n                                        } catch (e: Exception) {\n                                            errorWait = true\n                                            Log.e(\n                                                \"Compressor\",\n                                                e.message ?: \"Compression failed at swapping buffer\"\n                                            )\n                                        }\n\n                                        if (!errorWait) {\n                                            outputSurface.drawImage()\n\n                                            inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)\n\n                                            compressionProgressListener.onProgressChanged(\n                                                id,\n                                                bufferInfo.presentationTimeUs.toFloat() / duration.toFloat() * 100\n                                            )\n\n                                            inputSurface.swapBuffers()\n                                        }\n                                    }\n                                    if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {\n                                        decoderOutputAvailable = false\n                                        encoder.signalEndOfInputStream()\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                } catch (exception: Exception) {\n                    printException(exception)\n                    return Result(id, success = false, failureMessage = exception.message)\n                }\n\n                dispose(\n                    videoIndex,\n                    decoder,\n                    encoder,\n                    inputSurface,\n                    outputSurface,\n                    extractor\n                )\n\n                processAudio(\n                    mediaMuxer = mediaMuxer,\n                    bufferInfo = bufferInfo,\n                    disableAudio = disableAudio,\n                    extractor\n                )\n\n                extractor.release()\n                try {\n                    mediaMuxer.finishMovie()\n                } catch (e: Exception) {\n                    printException(e)\n                }\n\n            } catch (exception: Exception) {\n                printException(exception)\n            }\n\n            var resultFile = cacheFile\n\n            streamableFile?.let {\n                try {\n                    val result = StreamableVideo.start(`in` = cacheFile, out = File(it))\n                    resultFile = File(it)\n                    if (result && cacheFile.exists()) {\n                        cacheFile.delete()\n                    }\n\n                } catch (e: Exception) {\n                    printException(e)\n                }\n            }\n            return Result(\n                id,\n                success = true,\n                failureMessage = null,\n                size = resultFile.length(),\n                resultFile.path\n            )\n        }\n\n        return Result(\n            id,\n            success = false,\n            failureMessage = \"Something went wrong, please try again\"\n        )\n    }\n\n    private fun processAudio(\n        mediaMuxer: MP4Builder,\n        bufferInfo: MediaCodec.BufferInfo,\n        disableAudio: Boolean,\n        extractor: MediaExtractor\n    ) {\n        val audioIndex = findTrack(extractor, isVideo = false)\n        if (audioIndex >= 0 && !disableAudio) {\n            extractor.selectTrack(audioIndex)\n            val audioFormat = extractor.getTrackFormat(audioIndex)\n            val muxerTrackIndex = mediaMuxer.addTrack(audioFormat, true)\n            var maxBufferSize = audioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)\n\n            if (maxBufferSize <= 0) {\n                maxBufferSize = 64 * 1024\n            }\n\n            var buffer: ByteBuffer = ByteBuffer.allocateDirect(maxBufferSize)\n            if (Build.VERSION.SDK_INT >= 28) {\n                val size = extractor.sampleSize\n                if (size > maxBufferSize) {\n                    maxBufferSize = (size + 1024).toInt()\n                    buffer = ByteBuffer.allocateDirect(maxBufferSize)\n                }\n            }\n            var inputDone = false\n            extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)\n\n            while (!inputDone) {\n                val index = extractor.sampleTrackIndex\n                if (index == audioIndex) {\n                    bufferInfo.size = extractor.readSampleData(buffer, 0)\n\n                    if (bufferInfo.size >= 0) {\n                        bufferInfo.apply {\n                            presentationTimeUs = extractor.sampleTime\n                            offset = 0\n                            flags = MediaCodec.BUFFER_FLAG_KEY_FRAME\n                        }\n                        mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo, true)\n                        extractor.advance()\n\n                    } else {\n                        bufferInfo.size = 0\n                        inputDone = true\n                    }\n                } else if (index == -1) {\n                    inputDone = true\n                }\n            }\n            extractor.unselectTrack(audioIndex)\n        }\n    }\n\n    private fun prepareEncoder(outputFormat: MediaFormat, hasQTI: Boolean): MediaCodec {\n\n        // This seems to cause an issue with certain phones\n        // val encoderName = MediaCodecList(REGULAR_CODECS).findEncoderForFormat(outputFormat)\n        // val encoder: MediaCodec = MediaCodec.createByCodecName(encoderName)\n        // Log.i(\"encoderName\", encoder.name)\n        // c2.qti.avc.encoder results in a corrupted .mp4 video that does not play in\n        // Mac and iphones\n        var encoder = if (hasQTI) {\n            MediaCodec.createByCodecName(\"c2.android.avc.encoder\")\n        } else {\n            MediaCodec.createEncoderByType(MIME_TYPE)\n        }\n\n        try {\n            encoder.configure(\n                outputFormat, null, null,\n                MediaCodec.CONFIGURE_FLAG_ENCODE\n            )\n        } catch (e: Exception) {\n            encoder = MediaCodec.createEncoderByType(MIME_TYPE)\n            encoder.configure(\n                outputFormat, null, null,\n                MediaCodec.CONFIGURE_FLAG_ENCODE\n            )\n        }\n\n        return encoder\n    }\n\n    private fun prepareDecoder(\n        inputFormat: MediaFormat,\n        outputSurface: OutputSurface,\n    ): MediaCodec {\n        // This seems to cause an issue with certain phones\n        // val decoderName =\n        //    MediaCodecList(REGULAR_CODECS).findDecoderForFormat(inputFormat)\n        // val decoder = MediaCodec.createByCodecName(decoderName)\n        // Log.i(\"decoderName\", decoder.name)\n\n        // val decoder = if (hasQTI) {\n        // MediaCodec.createByCodecName(\"c2.android.avc.decoder\")\n        //} else {\n\n        val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)\n        //}\n\n        decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)\n\n        return decoder\n    }\n\n    private fun dispose(\n        videoIndex: Int,\n        decoder: MediaCodec,\n        encoder: MediaCodec,\n        inputSurface: InputSurface,\n        outputSurface: OutputSurface,\n        extractor: MediaExtractor\n    ) {\n        extractor.unselectTrack(videoIndex)\n\n        decoder.stop()\n        decoder.release()\n\n        encoder.stop()\n        encoder.release()\n\n        inputSurface.release()\n        outputSurface.release()\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.config\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport com.abedelazizshe.lightcompressorlibrary.VideoQuality\nimport com.abedelazizshe.lightcompressorlibrary.utils.saveVideoInExternal\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.IOException\n\ndata class Configuration(\n    var quality: VideoQuality = VideoQuality.MEDIUM,\n    var isMinBitrateCheckEnabled: Boolean = true,\n    var videoBitrateInMbps: Int? = null,\n    var disableAudio: Boolean = false,\n    val resizer: VideoResizer? = VideoResizer.auto,\n    var videoNames: List<String>\n) {\n    @Deprecated(\"Use VideoResizer to override the output video dimensions.\", ReplaceWith(\"Configuration(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, resizer = if (keepOriginalResolution) null else VideoResizer.auto, videoNames)\"))\n    constructor(\n        quality: VideoQuality = VideoQuality.MEDIUM,\n        isMinBitrateCheckEnabled: Boolean = true,\n        videoBitrateInMbps: Int? = null,\n        disableAudio: Boolean = false,\n        keepOriginalResolution: Boolean,\n        videoNames: List<String>) : this(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, getVideoResizer(keepOriginalResolution, null, null), videoNames)\n\n    @Deprecated(\"Use VideoResizer to override the output video dimensions.\", ReplaceWith(\"Configuration(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, resizer = VideoResizer.matchSize(videoWidth, videoHeight), videoNames)\"))\n    constructor(\n        quality: VideoQuality = VideoQuality.MEDIUM,\n        isMinBitrateCheckEnabled: Boolean = true,\n        videoBitrateInMbps: Int? = null,\n        disableAudio: Boolean = false,\n        keepOriginalResolution: Boolean = false,\n        videoHeight: Double? = null,\n        videoWidth: Double? = null,\n        videoNames: List<String>) : this(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, getVideoResizer(keepOriginalResolution, videoHeight, videoWidth), videoNames)\n}\n\nprivate fun getVideoResizer(keepOriginalResolution: Boolean, videoHeight: Double?, videoWidth: Double?): VideoResizer? =\n    if (keepOriginalResolution) {\n        null\n    } else if (videoWidth != null && videoHeight != null) {\n        VideoResizer.matchSize(videoWidth, videoHeight, true)\n    } else {\n        VideoResizer.auto\n    }\n\ninterface StorageConfiguration {\n    fun createFileToSave(\n        context: Context,\n        videoFile: File,\n        fileName: String,\n        shouldSave: Boolean\n    ): File\n}\n\nclass AppSpecificStorageConfiguration(\n    private val subFolderName: String? = null,\n) : StorageConfiguration {\n\n    override fun createFileToSave(\n        context: Context,\n        videoFile: File,\n        fileName: String,\n        shouldSave: Boolean\n    ): File {\n        val fullPath =\n            if (subFolderName != null) \"${subFolderName}/$fileName\"\n            else fileName\n\n        if (!File(\"${context.filesDir}/$fullPath\").exists()) {\n            File(\"${context.filesDir}/$fullPath\").parentFile?.mkdirs()\n        }\n        return File(context.filesDir, fullPath)\n    }\n}\n\n\nenum class SaveLocation {\n    pictures,\n    downloads,\n    movies,\n}\n\nclass SharedStorageConfiguration(\n    private val saveAt: SaveLocation? = null,\n    private val subFolderName: String? = null,\n) : StorageConfiguration {\n\n    override fun createFileToSave(\n        context: Context,\n        videoFile: File,\n        fileName: String,\n        shouldSave: Boolean\n    ): File {\n        val saveLocation =\n            when (saveAt) {\n                SaveLocation.downloads -> {\n                    Environment.DIRECTORY_DOWNLOADS\n                }\n\n                SaveLocation.pictures -> {\n                    Environment.DIRECTORY_PICTURES\n                }\n\n                else -> {\n                    Environment.DIRECTORY_MOVIES\n                }\n            }\n\n        if (Build.VERSION.SDK_INT >= 29) {\n            val fullPath =\n                if (subFolderName != null) \"$saveLocation/${subFolderName}\"\n                else saveLocation\n            if (shouldSave) {\n                saveVideoInExternal(context, fileName, fullPath, videoFile)\n                File(context.filesDir, fileName).delete()\n                return File(\"/storage/emulated/0/${fullPath}\", fileName)\n            }\n            return File(context.filesDir, fileName)\n        } else {\n            val savePath =\n                Environment.getExternalStoragePublicDirectory(saveLocation)\n\n            val fullPath =\n                if (subFolderName != null) \"$savePath/${subFolderName}\"\n                else savePath.path\n\n            val desFile = File(fullPath, fileName)\n\n            if (!desFile.exists()) {\n                try {\n                    desFile.parentFile?.mkdirs()\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n            }\n\n            if (shouldSave) {\n                context.openFileOutput(fileName, Context.MODE_PRIVATE)\n                    .use { outputStream ->\n                        FileInputStream(videoFile).use { inputStream ->\n                            val buf = ByteArray(4096)\n                            while (true) {\n                                val sz = inputStream.read(buf)\n                                if (sz <= 0) break\n                                outputStream.write(buf, 0, sz)\n                            }\n\n                        }\n                    }\n\n            }\n            return desFile\n        }\n    }\n}\n\nclass CacheStorageConfiguration(\n) : StorageConfiguration {\n    override fun createFileToSave(\n        context: Context,\n        videoFile: File,\n        fileName: String,\n        shouldSave: Boolean\n    ): File =\n        File.createTempFile(videoFile.nameWithoutExtension,videoFile.extension)\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/VideoResizer.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.config\n\nimport com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils\n\nfun interface VideoResizer {\n    companion object {\n        /**\n         * Shrinks the video's resolution based on its original width and height.\n         * - 50% If the width or height is greater than or equal to 1920 pixels.\n         * - 75% If the width or height is greater than or equal to 1280 pixels.\n         * - 95% If the width or height is greater than or equal to 960 pixels.\n         * - 90% If the width and height are both less than 960 pixels.\n         */\n        @JvmStatic\n        val auto: VideoResizer = ScaleResize(null);\n\n        /**\n         * Resize the video dimensions by the given scale factor\n         */\n        @JvmStatic\n        fun scale(value: Double): VideoResizer = ScaleResize(value)\n\n        /**\n         * Scale the video down if the width or height are greater than [limit], retaining the video's aspect ratio.\n         * @param limit The maximum width and height of the video\n         */\n        @JvmStatic\n        fun limitSize(limit: Double): VideoResizer = LimitDimension(limit, limit)\n\n        /**\n         * Scale the video down if the width or height are greater than [maxWidth] or [maxHeight], retaining the video's aspect ratio.\n         * @param maxWidth The maximum width of the video\n         * @param maxHeight The maximum height of the video\n         */\n        @JvmStatic\n        fun limitSize(maxWidth: Double, maxHeight: Double): VideoResizer = LimitDimension(maxWidth, maxHeight)\n\n        /**\n         * Scales the video so that the width and height matches [size], retaining the video's aspect ratio.\n         * @param size The target width/height of the video\n         */\n        @JvmStatic\n        fun matchSize(size: Double, stretch: Boolean = false): VideoResizer = MatchDimension(size, size, stretch)\n\n        /**\n         * Scales the video so that the width matches [width] or the height matches [height], retaining the video's aspect ratio.\n         * @param width The target width of the video\n         * @param height The target height of the video\n         */\n        @JvmStatic\n        fun matchSize(width: Double, height: Double, stretch: Boolean = false): VideoResizer = MatchDimension(width, height, stretch)\n\n        private fun keepAspect(width: Double, height: Double, newWidth: Double, newHeight: Double): Pair<Double, Double> {\n            val desiredAspect = width / height\n            val videoAspect = newWidth / newHeight\n            return if (videoAspect <= desiredAspect) Pair(newWidth, newWidth / desiredAspect) else Pair(newHeight * desiredAspect, newHeight)\n        }\n    }\n\n    fun resize(width: Double, height: Double): Pair<Double, Double>\n\n    private class LimitDimension(private val width: Double, private val height: Double) : VideoResizer {\n        override fun resize(width: Double, height: Double): Pair<Double, Double> {\n            return if (width < this.width && height < this.height) Pair(width, height) else keepAspect(width, height, this.width, this.height)\n        }\n    }\n\n    private class MatchDimension(private val width: Double, private val height: Double, private val stretch: Boolean) : VideoResizer {\n        override fun resize(width: Double, height: Double): Pair<Double, Double> {\n            return if (stretch) Pair(this.width, this.height) else keepAspect(width, height, this.width, this.height)\n        }\n    }\n\n    private class ScaleResize(private val percentage: Double? = null) : VideoResizer {\n        override fun resize(width: Double, height: Double): Pair<Double, Double> {\n            val p = percentage ?: CompressorUtils.autoResizePercentage(width, height)\n            return Pair(width * p, height * p)\n        }\n    }\n}"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/data/Atoms.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.data\n\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\n\n/*\nFOURCC is short for \"four character code\" - an identifier for a video codec, compression format,\ncolor or pixel format used in media files.\n\nA character in this context is a 1 byte/8 bit value, thus a FOURCC always takes up exactly 32 bits/4\nbytes in a file.\n*/\nfun fourCcToInt(byteArray: ByteArray): Int {\n    // The bytes of a byteArray value are ordered from most significant to least significant.\n    return ByteBuffer.wrap(byteArray).order(ByteOrder.BIG_ENDIAN).int\n}\n\n// Unused space available in file.\nval FREE_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'f'.code.toByte(),\n            'r'.code.toByte(),\n            'e'.code.toByte(),\n            'e'.code.toByte()\n        )\n    )\n\nval JUNK_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'j'.code.toByte(),\n            'u'.code.toByte(),\n            'n'.code.toByte(),\n            'k'.code.toByte()\n        )\n    )\n\n// Movie sample data— media samples such as video frames and groups of audio samples. Usually this\n// data can be interpreted only by using the movie resource.\nval MDAT_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'm'.code.toByte(),\n            'd'.code.toByte(),\n            'a'.code.toByte(),\n            't'.code.toByte()\n        )\n    )\n\n// Movie resource metadata about the movie (number and type of tracks, location of sample data,\n// and so on). Describes where the movie data can be found and how to interpret it.\nval MOOV_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'm'.code.toByte(),\n            'o'.code.toByte(),\n            'o'.code.toByte(),\n            'v'.code.toByte()\n        )\n    )\n\n// Reference to movie preview data.\nval PNOT_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'p'.code.toByte(),\n            'n'.code.toByte(),\n            'o'.code.toByte(),\n            't'.code.toByte()\n        )\n    )\n\n// Unused space available in file.\nval SKIP_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            's'.code.toByte(),\n            'k'.code.toByte(),\n            'i'.code.toByte(),\n            'p'.code.toByte()\n        )\n    )\n\n// Reserved space—can be overwritten by an extended size field if the following atom exceeds 2^32\n// bytes, without displacing the contents of the following atom.\nval WIDE_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'w'.code.toByte(),\n            'i'.code.toByte(),\n            'd'.code.toByte(),\n            'e'.code.toByte()\n        )\n    )\n\nval PICT_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'P'.code.toByte(),\n            'I'.code.toByte(),\n            'C'.code.toByte(),\n            'T'.code.toByte()\n        )\n    )\n\n// File type compatibility— identifies the file type and differentiates it from similar file\n// types, such as MPEG-4 files and JPEG-2000 files.\nval FTYP_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'f'.code.toByte(),\n            't'.code.toByte(),\n            'y'.code.toByte(),\n            'p'.code.toByte()\n        )\n    )\n\nval UUID_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'u'.code.toByte(),\n            'u'.code.toByte(),\n            'i'.code.toByte(),\n            'd'.code.toByte()\n        )\n    )\n\nval CMOV_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'c'.code.toByte(),\n            'm'.code.toByte(),\n            'o'.code.toByte(),\n            'v'.code.toByte()\n        )\n    )\n\nval STCO_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            's'.code.toByte(),\n            't'.code.toByte(),\n            'c'.code.toByte(),\n            'o'.code.toByte()\n        )\n    )\n\nval CO64_ATOM =\n    fourCcToInt(\n        byteArrayOf(\n            'c'.code.toByte(),\n            'o'.code.toByte(),\n            '6'.code.toByte(),\n            '4'.code.toByte()\n        )\n    )\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/CompressorUtils.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport android.media.MediaCodecInfo\nimport android.media.MediaCodecList\nimport android.media.MediaExtractor\nimport android.media.MediaFormat\nimport android.media.MediaMetadataRetriever\nimport android.os.Build\nimport android.util.Log\nimport com.abedelazizshe.lightcompressorlibrary.VideoQuality\nimport com.abedelazizshe.lightcompressorlibrary.video.Mp4Movie\nimport java.io.File\nimport kotlin.math.roundToInt\n\nobject CompressorUtils {\n\n    private const val MIN_HEIGHT = 640.0\n    private const val MIN_WIDTH = 368.0\n\n    // 1 second between I-frames\n    private const val I_FRAME_INTERVAL = 1\n\n    fun prepareVideoWidth(\n        mediaMetadataRetriever: MediaMetadataRetriever,\n    ): Double {\n        val widthData =\n            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)\n        return if (widthData.isNullOrEmpty()) {\n            MIN_WIDTH\n        } else {\n            widthData.toDouble()\n        }\n    }\n\n    fun prepareVideoHeight(\n        mediaMetadataRetriever: MediaMetadataRetriever,\n    ): Double {\n        val heightData =\n            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)\n        return if (heightData.isNullOrEmpty()) {\n            MIN_HEIGHT\n        } else {\n            heightData.toDouble()\n        }\n    }\n\n    /**\n     * Setup movie with the height, width, and rotation values\n     * @param rotation video rotation\n     *\n     * @return set movie with new values\n     */\n    fun setUpMP4Movie(\n        rotation: Int,\n        cacheFile: File,\n    ): Mp4Movie {\n        val movie = Mp4Movie()\n        movie.apply {\n            setCacheFile(cacheFile)\n            setRotation(rotation)\n        }\n\n        return movie\n    }\n\n    /**\n     * Set output parameters like bitrate and frame rate\n     */\n    fun setOutputFileParameters(\n        inputFormat: MediaFormat,\n        outputFormat: MediaFormat,\n        newBitrate: Int,\n    ) {\n        val newFrameRate = getFrameRate(inputFormat)\n        val iFrameInterval = getIFrameIntervalRate(inputFormat)\n        outputFormat.apply {\n\n            // according to https://developer.android.com/media/optimize/sharing#b-frames_and_encoding_profiles\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                val type = outputFormat.getString(MediaFormat.KEY_MIME)\n                val higherLevel = getHighestCodecProfileLevel(type)\n                Log.i(\"Output file parameters\", \"Selected CodecProfileLevel: $higherLevel\")\n                setInteger(MediaFormat.KEY_PROFILE, higherLevel)\n            } else {\n                setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)\n            }\n\n            setInteger(\n                MediaFormat.KEY_COLOR_FORMAT,\n                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface\n            )\n\n            setInteger(MediaFormat.KEY_FRAME_RATE, newFrameRate)\n            setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)\n            // expected bps\n            setInteger(MediaFormat.KEY_BIT_RATE, newBitrate)\n            setInteger(\n                MediaFormat.KEY_BITRATE_MODE,\n                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR\n            )\n\n\n\n            getColorStandard(inputFormat)?.let {\n                setInteger(MediaFormat.KEY_COLOR_STANDARD, it)\n            }\n\n            getColorTransfer(inputFormat)?.let {\n                setInteger(MediaFormat.KEY_COLOR_TRANSFER, it)\n            }\n\n            getColorRange(inputFormat)?.let {\n                setInteger(MediaFormat.KEY_COLOR_RANGE, it)\n            }\n\n\n            Log.i(\n                \"Output file parameters\",\n                \"videoFormat: $this\"\n            )\n        }\n    }\n\n    private fun getFrameRate(format: MediaFormat): Int {\n        return if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) format.getInteger(MediaFormat.KEY_FRAME_RATE)\n        else 30\n    }\n\n    private fun getIFrameIntervalRate(format: MediaFormat): Int {\n        return if (format.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) format.getInteger(\n            MediaFormat.KEY_I_FRAME_INTERVAL\n        )\n        else I_FRAME_INTERVAL\n    }\n\n    private fun getColorStandard(format: MediaFormat): Int? {\n        return if (format.containsKey(MediaFormat.KEY_COLOR_STANDARD)) format.getInteger(\n            MediaFormat.KEY_COLOR_STANDARD\n        )\n        else null\n    }\n\n    private fun getColorTransfer(format: MediaFormat): Int? {\n        return if (format.containsKey(MediaFormat.KEY_COLOR_TRANSFER)) format.getInteger(\n            MediaFormat.KEY_COLOR_TRANSFER\n        )\n        else null\n    }\n\n    private fun getColorRange(format: MediaFormat): Int? {\n        return if (format.containsKey(MediaFormat.KEY_COLOR_RANGE)) format.getInteger(\n            MediaFormat.KEY_COLOR_RANGE\n        )\n        else null\n    }\n\n    /**\n     * Counts the number of tracks (video, audio) found in the file source provided\n     * @param extractor what is used to extract the encoded data\n     * @param isVideo to determine whether we are processing video or audio at time of call\n     * @return index of the requested track\n     */\n    fun findTrack(\n        extractor: MediaExtractor,\n        isVideo: Boolean,\n    ): Int {\n        val numTracks = extractor.trackCount\n        for (i in 0 until numTracks) {\n            val format = extractor.getTrackFormat(i)\n            val mime = format.getString(MediaFormat.KEY_MIME)\n            if (isVideo) {\n                if (mime?.startsWith(\"video/\")!!) return i\n            } else {\n                if (mime?.startsWith(\"audio/\")!!) return i\n            }\n        }\n        return -5\n    }\n\n    fun printException(exception: Exception) {\n        var message = \"An error has occurred!\"\n        exception.localizedMessage?.let {\n            message = it\n        }\n        Log.e(\"Compressor\", message, exception)\n    }\n\n    /**\n     * Get fixed bitrate value based on the file's current bitrate\n     * @param bitrate file's current bitrate\n     * @return new smaller bitrate value\n     */\n    fun getBitrate(\n        bitrate: Int,\n        quality: VideoQuality,\n    ): Int {\n        return when (quality) {\n            VideoQuality.VERY_LOW -> (bitrate * 0.1).roundToInt()\n            VideoQuality.LOW -> (bitrate * 0.2).roundToInt()\n            VideoQuality.MEDIUM -> (bitrate * 0.3).roundToInt()\n            VideoQuality.HIGH -> (bitrate * 0.4).roundToInt()\n            VideoQuality.VERY_HIGH -> (bitrate * 0.6).roundToInt()\n        }\n    }\n\n    /**\n     * Generate new width and height for source file\n     * @param width file's original width\n     * @param height file's original height\n     * @return the scale factor to apply to the video's resolution\n     */\n    fun autoResizePercentage(width: Double, height: Double): Double {\n        return when {\n            width >= 1920 || height >= 1920 -> 0.5\n            width >= 1280 || height >= 1280 -> 0.75\n            width >= 960 || height >= 960 -> 0.95\n            else -> 0.9\n        }\n    }\n\n    fun hasQTI(): Boolean {\n        val list = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos\n        for (codec in list) {\n            Log.i(\"CODECS: \", codec.name)\n            if (codec.name.contains(\"qti.avc\")) {\n                return true\n            }\n        }\n        return false\n    }\n\n    /**\n     * Get the highest profile level supported by the AVC encoder: High > Main > Baseline\n     */\n    private fun getHighestCodecProfileLevel(type: String?): Int {\n        if (type == null) {\n            return MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline\n        }\n        val list = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos\n        val capabilities = list\n            .filter { codec -> type in codec.supportedTypes && codec.name.contains(\"encoder\") }\n            .mapNotNull { codec -> codec.getCapabilitiesForType(type) }\n\n        capabilities.forEach { capabilitiesForType ->\n            val levels =  capabilitiesForType.profileLevels.map { it.profile }\n            return when {\n                MediaCodecInfo.CodecProfileLevel.AVCProfileHigh in levels -> MediaCodecInfo.CodecProfileLevel.AVCProfileHigh\n                MediaCodecInfo.CodecProfileLevel.AVCProfileMain in levels -> MediaCodecInfo.CodecProfileLevel.AVCProfileMain\n                else -> MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline\n            }\n        }\n\n        return MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline\n    }\n}\n\n/*\n            if (codec.name == \"c2.qti.avc.encoder\") {\n                val capabilities = codec.getCapabilitiesForType(\"video/avc\")\n\n\n                for (c in capabilities.colorFormats) {\n                    Log.wtf(\"color format\", c.toString())\n                }\n\n                for (c in capabilities.profileLevels) {\n                    Log.wtf(\" level\", c.level.toString())\n                    Log.wtf(\"profile \", c.profile.toString())\n                }\n\n                Log.wtf(\n                    \"complexity range\",\n                    capabilities.encoderCapabilities.complexityRange.upper.toString()\n                )\n\n                Log.wtf(\n                    \"quality range\", \" ${ capabilities.encoderCapabilities.qualityRange}\"\n                )\n\n                Log.wtf(\n                    \"frame rates range\", \" ${ capabilities.videoCapabilities.supportedFrameRates}\"\n                )\n\n                Log.wtf(\n                    \"bitrate rates range\", \" ${ capabilities.videoCapabilities.bitrateRange}\"\n                )\n\n                Log.wtf(\n                    \"mode supported\", \" ${ capabilities.encoderCapabilities.isBitrateModeSupported(1)}\"\n                )\n\n                Log.wtf(\n                    \"height alignment\", \" ${ capabilities.videoCapabilities.heightAlignment}\"\n                )\n\n                Log.wtf(\n                    \"supported heights\", \" ${ capabilities.videoCapabilities.supportedHeights}\"\n                )\n\n                Log.wtf(\n                    \"supported points\", \" ${ capabilities.videoCapabilities.supportedPerformancePoints}\"\n                )\n\n                Log.wtf(\n                    \"supported width\", \" ${ capabilities.videoCapabilities.supportedWidths}\"\n                )\n\n                Log.wtf(\n                    \"width alignment\", \" ${ capabilities.videoCapabilities.widthAlignment}\"\n                )\n\n                Log.wtf(\n                    \"default format\", \" ${ capabilities.defaultFormat}\"\n                )\n\n                Log.wtf(\n                    \"mime\", \" ${ capabilities.mimeType}\"\n                )\n\n            }\n */"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/FileUtils.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.os.Environment\nimport android.provider.MediaStore\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\n\nfun saveVideoInExternal(\n    context: Context,\n    videoFileName: String,\n    saveLocation: String,\n    videoFile: File\n) {\n    val values = ContentValues().apply {\n\n        put(\n            MediaStore.Images.Media.DISPLAY_NAME,\n            videoFileName\n        )\n        put(MediaStore.Images.Media.MIME_TYPE, \"video/mp4\")\n        put(MediaStore.Images.Media.RELATIVE_PATH, saveLocation)\n        put(MediaStore.Images.Media.IS_PENDING, 1)\n    }\n\n    var collection =\n        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)\n\n    if (saveLocation == Environment.DIRECTORY_DOWNLOADS) {\n        collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI\n    }\n\n    val fileUri = context.contentResolver.insert(collection, values)\n\n    fileUri?.let {\n        context.contentResolver.openFileDescriptor(fileUri, \"rw\")\n            .use { descriptor ->\n                descriptor?.let {\n                    FileOutputStream(descriptor.fileDescriptor).use { out ->\n                        FileInputStream(videoFile).use { inputStream ->\n                            val buf = ByteArray(4096)\n                            while (true) {\n                                val sz = inputStream.read(buf)\n                                if (sz <= 0) break\n                                out.write(buf, 0, sz)\n                            }\n                        }\n                    }\n                }\n            }\n\n        values.clear()\n        values.put(MediaStore.Video.Media.IS_PENDING, 0)\n        context.contentResolver.update(fileUri, values, null, null)\n    }\n}\n\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/NumbersUtils.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport kotlin.math.roundToInt\n\nfun uInt32ToLong(int32: Int): Long {\n    return int32.toLong()\n}\n\nfun uInt32ToInt(uInt32: Long): Int {\n    if (uInt32 > Int.MAX_VALUE || uInt32 < 0) {\n        throw Exception(\"uInt32 value is too large\")\n    }\n    return uInt32.toInt()\n}\n\nfun uInt64ToLong(uInt64: Long): Long {\n    if (uInt64 < 0) throw Exception(\"uInt64 value is too large\")\n    return uInt64\n}\n\n\nfun uInt32ToInt(uInt32: Int): Int {\n    if (uInt32 < 0) {\n        throw Exception(\"uInt32 value is too large\")\n    }\n    return uInt32\n}\n\nprivate fun roundEven(value: Int): Int = value + 1 and 1.inv()\n\nfun roundDimension(value: Double): Int =\n    roundEven(((value / 16).roundToInt() * 16))\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/StreamableVideo.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport android.util.Log\nimport com.abedelazizshe.lightcompressorlibrary.data.*\nimport java.io.*\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\nimport java.nio.channels.FileChannel\n\nobject StreamableVideo {\n\n    private const val tag = \"StreamableVideo\"\n    private const val ATOM_PREAMBLE_SIZE = 8\n\n    /**\n     * @param in  Input file.\n     * @param out Output file.\n     * @return false if input file is already fast start.\n     * @throws IOException\n     */\n    @Throws(IOException::class)\n    fun start(`in`: File?, out: File): Boolean {\n        var ret = false\n        var inStream: FileInputStream? = null\n        var outStream: FileOutputStream? = null\n        return try {\n            inStream = FileInputStream(`in`)\n            val infile = inStream.channel\n            outStream = FileOutputStream(out)\n            val outfile = outStream.channel\n            convert(infile, outfile).also { ret = it }\n        } finally {\n            safeClose(inStream)\n            safeClose(outStream)\n            if (!ret) {\n                out.delete()\n            }\n        }\n    }\n\n    @Throws(IOException::class)\n    private fun convert(infile: FileChannel, outfile: FileChannel): Boolean {\n        val atomBytes = ByteBuffer.allocate(ATOM_PREAMBLE_SIZE).order(ByteOrder.BIG_ENDIAN)\n        var atomType = 0\n        var atomSize: Long = 0\n        val lastOffset: Long\n        val moovAtom: ByteBuffer\n        var ftypAtom: ByteBuffer? = null\n        var startOffset: Long = 0\n\n        // traverse through the atoms in the file to make sure that 'moov' is at the end\n        while (readAndFill(infile, atomBytes)) {\n            atomSize = uInt32ToLong(atomBytes.int)\n            atomType = atomBytes.int\n\n            // keep ftyp atom\n            if (atomType == FTYP_ATOM) {\n                val ftypAtomSize = uInt32ToInt(atomSize)\n                ftypAtom = ByteBuffer.allocate(ftypAtomSize).order(ByteOrder.BIG_ENDIAN)\n                atomBytes.rewind()\n                ftypAtom.put(atomBytes)\n                if (infile.read(ftypAtom) < ftypAtomSize - ATOM_PREAMBLE_SIZE) break\n                ftypAtom.flip()\n                startOffset = infile.position() // after ftyp atom\n            } else {\n                if (atomSize == 1L) {\n                    /* 64-bit special case */\n                    atomBytes.clear()\n                    if (!readAndFill(infile, atomBytes)) break\n                    atomSize = uInt64ToLong(atomBytes.long)\n                    infile.position(infile.position() + atomSize - ATOM_PREAMBLE_SIZE * 2) // seek\n                } else {\n                    infile.position(infile.position() + atomSize - ATOM_PREAMBLE_SIZE) // seek\n                }\n            }\n            if (atomType != FREE_ATOM\n                && atomType != JUNK_ATOM\n                && atomType != MDAT_ATOM\n                && atomType != MOOV_ATOM\n                && atomType != PNOT_ATOM\n                && atomType != SKIP_ATOM\n                && atomType != WIDE_ATOM\n                && atomType != PICT_ATOM\n                && atomType != UUID_ATOM\n                && atomType != FTYP_ATOM\n            ) {\n                Log.wtf(tag, \"encountered non-QT top-level atom (is this a QuickTime file?)\")\n                break\n            }\n\n            /* The atom header is 8 (or 16 bytes), if the atom size (which\n         * includes these 8 or 16 bytes) is less than that, we won't be\n         * able to continue scanning sensibly after this atom, so break. */\n            if (atomSize < 8) break\n        }\n        if (atomType != MOOV_ATOM) {\n            Log.wtf(tag, \"last atom in file was not a moov atom\")\n            return false\n        }\n\n        // atomSize is uint64, but for moov uint32 should be stored.\n        val moovAtomSize: Int = uInt32ToInt(atomSize)\n        lastOffset =\n            infile.size() - moovAtomSize\n        moovAtom = ByteBuffer.allocate(moovAtomSize).order(ByteOrder.BIG_ENDIAN)\n        if (!readAndFill(infile, moovAtom, lastOffset)) {\n            throw Exception(\"failed to read moov atom\")\n        }\n\n        if (moovAtom.getInt(12) == CMOV_ATOM) {\n            throw Exception(\"this utility does not support compressed moov atoms yet\")\n        }\n\n        // crawl through the moov chunk in search of stco or co64 atoms\n        while (moovAtom.remaining() >= 8) {\n            val atomHead = moovAtom.position()\n            atomType = moovAtom.getInt(atomHead + 4)\n            if (!(atomType == STCO_ATOM || atomType == CO64_ATOM)) {\n                moovAtom.position(moovAtom.position() + 1)\n                continue\n            }\n            atomSize = uInt32ToLong(moovAtom.getInt(atomHead)) // uint32\n            if (atomSize > moovAtom.remaining()) {\n                throw Exception(\"bad atom size\")\n            }\n            // skip size (4 bytes), type (4 bytes), version (1 byte) and flags (3 bytes)\n            moovAtom.position(atomHead + 12)\n            if (moovAtom.remaining() < 4) {\n                throw Exception(\"malformed atom\")\n            }\n            // uint32_t, but assuming moovAtomSize is in int32 range, so this will be in int32 range\n            val offsetCount = uInt32ToInt(moovAtom.int)\n            if (atomType == STCO_ATOM) {\n                Log.i(tag, \"patching stco atom...\")\n                if (moovAtom.remaining() < offsetCount * 4) {\n                    throw Exception(\"bad atom size/element count\")\n                }\n                for (i in 0 until offsetCount) {\n                    val currentOffset = moovAtom.getInt(moovAtom.position())\n                    val newOffset =\n                        currentOffset + moovAtomSize // calculate uint32 in int, bitwise addition\n\n                    if (currentOffset < 0 && newOffset >= 0) {\n                        throw Exception(\n                            \"This is bug in original qt-faststart.c: \"\n                                    + \"stco atom should be extended to co64 atom as new offset value overflows uint32, \"\n                                    + \"but is not implemented.\"\n                        )\n                    }\n                    moovAtom.putInt(newOffset)\n                }\n            } else if (atomType == CO64_ATOM) {\n                Log.wtf(tag, \"patching co64 atom...\")\n                if (moovAtom.remaining() < offsetCount * 8) {\n                    throw Exception(\"bad atom size/element count\")\n                }\n                for (i in 0 until offsetCount) {\n                    val currentOffset = moovAtom.getLong(moovAtom.position())\n                    moovAtom.putLong(currentOffset + moovAtomSize) // calculate uint64 in long, bitwise addition\n                }\n            }\n        }\n        infile.position(startOffset) // seek after ftyp atom\n        if (ftypAtom != null) {\n            // dump the same ftyp atom\n            Log.i(tag, \"writing ftyp atom...\")\n            ftypAtom.rewind()\n            outfile.write(ftypAtom)\n        }\n\n        // dump the new moov atom\n        Log.i(tag, \"writing moov atom...\")\n        moovAtom.rewind()\n        outfile.write(moovAtom)\n\n        // copy the remainder of the infile, from offset 0 -> (lastOffset - startOffset) - 1\n        Log.i(tag, \"copying rest of file...\")\n        infile.transferTo(startOffset, lastOffset - startOffset, outfile)\n        return true\n    }\n\n    private fun safeClose(closeable: Closeable?) {\n        if (closeable != null) {\n            try {\n                closeable.close()\n            } catch (e: IOException) {\n                Log.wtf(tag, \"Failed to close file: \")\n            }\n        }\n    }\n\n    @Throws(IOException::class)\n    private fun readAndFill(infile: FileChannel, buffer: ByteBuffer): Boolean {\n        buffer.clear()\n        val size = infile.read(buffer)\n        buffer.flip()\n        return size == buffer.capacity()\n    }\n\n    @Throws(IOException::class)\n    private fun readAndFill(infile: FileChannel, buffer: ByteBuffer, position: Long): Boolean {\n        buffer.clear()\n        val size = infile.read(buffer, position)\n        buffer.flip()\n        return size == buffer.capacity()\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/InputSurface.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.opengl.*\nimport android.view.Surface\n\nclass InputSurface(surface: Surface?) {\n\n    private val eglRecordableAndroid = 0x3142\n    private val eglOpenGlES2Bit = 4\n    private var mEGLDisplay: EGLDisplay? = null\n    private var mEGLContext: EGLContext? = null\n    private var mEGLSurface: EGLSurface? = null\n    private var mSurface: Surface? = null\n\n    init {\n        if (surface == null) {\n            throw NullPointerException()\n        }\n        mSurface = surface\n        eglSetup()\n    }\n\n    private fun eglSetup() {\n        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)\n        if (mEGLDisplay === EGL14.EGL_NO_DISPLAY) {\n            throw RuntimeException(\"unable to get EGL14 display\")\n        }\n        val version = IntArray(2)\n        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {\n            mEGLDisplay = null\n            throw RuntimeException(\"unable to initialize EGL14\")\n        }\n        val attribList = intArrayOf(\n            EGL14.EGL_RED_SIZE,\n            8,\n            EGL14.EGL_GREEN_SIZE,\n            8,\n            EGL14.EGL_BLUE_SIZE,\n            8,\n            EGL14.EGL_RENDERABLE_TYPE,\n            eglOpenGlES2Bit,\n            eglRecordableAndroid,\n            1,\n            EGL14.EGL_NONE,\n        )\n        val configs = arrayOfNulls<EGLConfig>(1)\n        val numConfigs = IntArray(1)\n        if (!EGL14.eglChooseConfig(\n                mEGLDisplay, attribList, 0, configs, 0, configs.size,\n                numConfigs, 0\n            )\n        ) {\n            throw RuntimeException(\"unable to find RGB888+recordable ES2 EGL config\")\n        }\n        val attrs = intArrayOf(\n            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,\n            EGL14.EGL_NONE\n        )\n        mEGLContext =\n            EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrs, 0)\n        checkEglError()\n        if (mEGLContext == null) {\n            throw RuntimeException(\"null context\")\n        }\n        val surfaceAttrs = intArrayOf(\n            EGL14.EGL_NONE\n        )\n        mEGLSurface = EGL14.eglCreateWindowSurface(\n            mEGLDisplay, configs[0], mSurface,\n            surfaceAttrs, 0\n        )\n        checkEglError()\n        if (mEGLSurface == null) {\n            throw RuntimeException(\"surface was null\")\n        }\n    }\n\n    fun release() {\n        if (EGL14.eglGetCurrentContext() == mEGLContext) {\n            EGL14.eglMakeCurrent(\n                mEGLDisplay,\n                EGL14.EGL_NO_SURFACE,\n                EGL14.EGL_NO_SURFACE,\n                EGL14.EGL_NO_CONTEXT\n            )\n        }\n        EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface)\n        EGL14.eglDestroyContext(mEGLDisplay, mEGLContext)\n\n        mSurface?.release()\n\n        mEGLDisplay = null\n        mEGLContext = null\n        mEGLSurface = null\n\n        mSurface = null\n    }\n\n    fun makeCurrent() {\n        if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {\n            throw RuntimeException(\"eglMakeCurrent failed\")\n        }\n    }\n\n    fun swapBuffers(): Boolean =\n        EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface)\n\n\n    fun setPresentationTime(nsecs: Long) {\n        EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs)\n    }\n\n    private fun checkEglError() {\n        var failed = false\n        while (EGL14.eglGetError() != EGL14.EGL_SUCCESS) {\n            failed = true\n        }\n        if (failed) {\n            throw RuntimeException(\"EGL error encountered (see log)\")\n        }\n    }\n\n}"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/MP4Builder.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.media.MediaCodec\nimport android.media.MediaFormat\nimport com.coremedia.iso.boxes.*\nimport com.googlecode.mp4parser.util.Matrix\nimport java.io.FileOutputStream\nimport java.nio.ByteBuffer\nimport java.nio.channels.FileChannel\nimport java.util.*\n\nclass MP4Builder {\n\n    private lateinit var mdat: Mdat\n    private lateinit var currentMp4Movie: Mp4Movie\n    private lateinit var fos: FileOutputStream\n    private lateinit var fc: FileChannel\n    private var dataOffset: Long = 0\n    private var wroteSinceLastMdat: Long = 0\n    private var writeNewMdat = true\n    private val track2SampleSizes = HashMap<Track, LongArray>()\n    private lateinit var sizeBuffer: ByteBuffer\n\n    @Throws(Exception::class)\n    fun createMovie(mp4Movie: Mp4Movie): MP4Builder {\n        currentMp4Movie = mp4Movie\n\n        fos = FileOutputStream(mp4Movie.getCacheFile())\n        fc = fos.channel\n\n        val fileTypeBox: FileTypeBox = createFileTypeBox()\n        fileTypeBox.getBox(fc)\n        dataOffset += fileTypeBox.size\n        wroteSinceLastMdat = dataOffset\n\n        mdat = Mdat()\n        sizeBuffer = ByteBuffer.allocateDirect(4)\n\n        return this\n    }\n\n    @Throws(Exception::class)\n    private fun flushCurrentMdat() {\n        val oldPosition = fc.position()\n        fc.position(mdat.offset)\n        mdat.getBox(fc)\n        fc.position(oldPosition)\n        mdat.setDataOffset(0)\n        mdat.setContentSize(0)\n        fos.flush()\n    }\n\n    @Throws(Exception::class)\n    fun writeSampleData(\n        trackIndex: Int,\n        byteBuf: ByteBuffer,\n        bufferInfo: MediaCodec.BufferInfo,\n        isAudio: Boolean\n    ) {\n\n        if (writeNewMdat) {\n            mdat.apply {\n                setContentSize(0)\n                getBox(fc)\n                setDataOffset(dataOffset)\n            }\n            dataOffset += 16\n            wroteSinceLastMdat += 16\n            writeNewMdat = false\n        }\n\n        mdat.setContentSize(mdat.getContentSize() + bufferInfo.size)\n        wroteSinceLastMdat += bufferInfo.size.toLong()\n\n        var flush = false\n        if (wroteSinceLastMdat >= 32 * 1024) {\n            flushCurrentMdat()\n            writeNewMdat = true\n            flush = true\n            wroteSinceLastMdat = 0\n        }\n\n        currentMp4Movie.addSample(trackIndex, dataOffset, bufferInfo)\n\n        if (!isAudio) {\n            byteBuf.position(bufferInfo.offset + 4)\n            byteBuf.limit(bufferInfo.offset + bufferInfo.size)\n\n            sizeBuffer.position(0)\n            sizeBuffer.putInt(bufferInfo.size - 4)\n            sizeBuffer.position(0)\n            fc.write(sizeBuffer)\n        } else {\n            byteBuf.position(bufferInfo.offset + 0)\n            byteBuf.limit(bufferInfo.offset + bufferInfo.size)\n        }\n\n        fc.write(byteBuf)\n        dataOffset += bufferInfo.size.toLong()\n\n        if (flush) {\n            fos.flush()\n        }\n    }\n\n    fun addTrack(mediaFormat: MediaFormat, isAudio: Boolean): Int =\n        currentMp4Movie.addTrack(mediaFormat, isAudio)\n\n    @Throws(Exception::class)\n    fun finishMovie() {\n        if (mdat.getContentSize() != 0L) {\n            flushCurrentMdat()\n        }\n\n        for (track in currentMp4Movie.getTracks()) {\n            val samples: List<Sample> = track.getSamples()\n            val sizes = LongArray(samples.size)\n            for (i in sizes.indices) {\n                sizes[i] = samples[i].size\n            }\n            track2SampleSizes[track] = sizes\n        }\n\n        val moov: Box = createMovieBox(currentMp4Movie)\n        moov.getBox(fc)\n\n        fos.flush()\n        fc.close()\n        fos.close()\n    }\n\n    private fun createFileTypeBox(): FileTypeBox {\n        // completed list can be found at https://www.ftyps.com/\n        val minorBrands = listOf(\n            \"isom\", \"iso2\", \"mp41\"\n        )\n\n        return FileTypeBox(\"mp42\", 0, minorBrands)\n    }\n\n    private fun gcd(a: Long, b: Long): Long {\n        return if (b == 0L) a\n        else gcd(b, a % b)\n    }\n\n    private fun getTimescale(mp4Movie: Mp4Movie): Long {\n        var timescale: Long = 0\n        if (mp4Movie.getTracks().isNotEmpty()) {\n            timescale = mp4Movie.getTracks().iterator().next().getTimeScale().toLong()\n        }\n\n        for (track in mp4Movie.getTracks()) {\n            timescale = gcd(\n                track.getTimeScale().toLong(),\n                timescale\n            )\n        }\n\n        return timescale\n    }\n\n    private fun createMovieBox(movie: Mp4Movie): MovieBox {\n        val movieBox = MovieBox()\n        val mvhd = MovieHeaderBox()\n\n        mvhd.apply {\n            creationTime = Date()\n            modificationTime = Date()\n            matrix = Matrix.ROTATE_0\n        }\n\n        val movieTimeScale = getTimescale(movie)\n        var duration: Long = 0\n\n        for (track in movie.getTracks()) {\n            val tracksDuration = track.getDuration() * movieTimeScale / track.getTimeScale()\n            if (tracksDuration > duration) {\n                duration = tracksDuration\n            }\n        }\n\n        mvhd.duration = duration\n        mvhd.timescale = movieTimeScale\n        mvhd.nextTrackId = (movie.getTracks().size + 1).toLong()\n        movieBox.addBox(mvhd)\n\n        for (track in movie.getTracks()) {\n            movieBox.addBox(createTrackBox(track, movie))\n        }\n\n        return movieBox\n    }\n\n    private fun createTrackBox(track: Track, movie: Mp4Movie): TrackBox {\n        val trackBox = TrackBox()\n        val tkhd = TrackHeaderBox()\n        tkhd.apply {\n            isEnabled = true\n            isInPreview = true\n            isInMovie = true\n            matrix = if (track.isAudio()) {\n                Matrix.ROTATE_0\n            } else {\n                movie.getMatrix()\n            }\n            alternateGroup = 0\n            creationTime = track.getCreationTime()\n            duration = track.getDuration() * getTimescale(movie) / track.getTimeScale()\n            height = track.getHeight().toDouble()\n            width = track.getWidth().toDouble()\n            layer = 0\n            modificationTime = Date()\n            trackId = track.getTrackId() + 1\n            volume = track.getVolume()\n        }\n        trackBox.addBox(tkhd)\n\n        val mdia = MediaBox()\n        trackBox.addBox(mdia)\n\n        val mdhd = MediaHeaderBox()\n        mdhd.apply {\n            creationTime = track.getCreationTime()\n            duration = track.getDuration()\n            timescale = track.getTimeScale().toLong()\n            language = \"eng\"\n        }\n        mdia.addBox(mdhd)\n\n        val hdlr = HandlerBox()\n        hdlr.apply {\n            name = if (track.isAudio()) \"SoundHandle\" else \"VideoHandle\"\n            handlerType = track.getHandler()\n        }\n        mdia.addBox(hdlr)\n\n        val minf = MediaInformationBox()\n        when {\n            track.getHandler() == \"vide\" -> {\n                minf.addBox(VideoMediaHeaderBox())\n            }\n\n            track.getHandler() == \"soun\" -> {\n                minf.addBox(SoundMediaHeaderBox())\n            }\n\n            track.getHandler() == \"text\" -> {\n                minf.addBox(NullMediaHeaderBox())\n            }\n\n            track.getHandler() == \"subt\" -> {\n                minf.addBox(SubtitleMediaHeaderBox())\n            }\n\n            track.getHandler() == \"hint\" -> {\n                minf.addBox(HintMediaHeaderBox())\n            }\n\n            track.getHandler() == \"sbtl\" -> {\n                minf.addBox(NullMediaHeaderBox())\n            }\n        }\n\n        val dinf = DataInformationBox()\n        val dref = DataReferenceBox()\n        dinf.addBox(dref)\n\n        val url = DataEntryUrlBox()\n        url.flags = 1\n\n        dref.addBox(url)\n        minf.addBox(dinf)\n\n        val stbl: Box = createStbl(track)\n        minf.addBox(stbl)\n        mdia.addBox(minf)\n\n        return trackBox\n    }\n\n    private fun createStbl(track: Track): Box {\n        val stbl = SampleTableBox()\n        createStsd(track, stbl)\n        createStts(track, stbl)\n        createStss(track, stbl)\n        createStsc(track, stbl)\n        createStsz(track, stbl)\n        createStco(track, stbl)\n        return stbl\n    }\n\n    private fun createStsd(track: Track, stbl: SampleTableBox) {\n        stbl.addBox(track.getSampleDescriptionBox())\n    }\n\n    private fun createStts(track: Track, stbl: SampleTableBox) {\n        var lastEntry: TimeToSampleBox.Entry? = null\n        val entries: MutableList<TimeToSampleBox.Entry> = ArrayList()\n        for (delta in track.getSampleDurations()) {\n            if (lastEntry != null && lastEntry.delta == delta) {\n                lastEntry.count = lastEntry.count + 1\n            } else {\n                lastEntry = TimeToSampleBox.Entry(1, delta)\n                entries.add(lastEntry)\n            }\n        }\n        val stts = TimeToSampleBox()\n        stts.entries = entries\n        stbl.addBox(stts)\n    }\n\n    private fun createStss(track: Track, stbl: SampleTableBox) {\n        val syncSamples = track.getSyncSamples()\n        if (syncSamples != null && syncSamples.isNotEmpty()) {\n            val stss = SyncSampleBox()\n            stss.sampleNumber = syncSamples\n            stbl.addBox(stss)\n        }\n    }\n\n    private fun createStsc(track: Track, stbl: SampleTableBox) {\n        val stsc = SampleToChunkBox()\n        stsc.entries = LinkedList()\n\n        var lastOffset: Long\n        var lastChunkNumber = 1\n        var lastSampleCount = 0\n        var previousWrittenChunkCount = -1\n\n        val samplesCount = track.getSamples().size\n        for (a in 0 until samplesCount) {\n            val sample = track.getSamples()[a]\n            val offset = sample.offset\n            val size = sample.size\n\n            lastOffset = offset + size\n            lastSampleCount++\n\n            var write = false\n            if (a != samplesCount - 1) {\n                val nextSample = track.getSamples()[a + 1]\n                if (lastOffset != nextSample.offset) {\n                    write = true\n                }\n            } else {\n                write = true\n            }\n\n            if (write) {\n                if (previousWrittenChunkCount != lastSampleCount) {\n                    stsc.entries.add(\n                        SampleToChunkBox.Entry(\n                            lastChunkNumber.toLong(),\n                            lastSampleCount.toLong(), 1\n                        )\n                    )\n                    previousWrittenChunkCount = lastSampleCount\n                }\n                lastSampleCount = 0\n                lastChunkNumber++\n            }\n        }\n        stbl.addBox(stsc)\n    }\n\n    private fun createStsz(track: Track, stbl: SampleTableBox) {\n        val stsz = SampleSizeBox()\n        stsz.sampleSizes = track2SampleSizes[track]\n        stbl.addBox(stsz)\n    }\n\n    private fun createStco(track: Track, stbl: SampleTableBox) {\n        val chunksOffsets = ArrayList<Long>()\n        var lastOffset: Long = -1\n        for (sample in track.getSamples()) {\n            val offset = sample.offset\n            if (lastOffset != -1L && lastOffset != offset) {\n                lastOffset = -1\n            }\n            if (lastOffset == -1L) {\n                chunksOffsets.add(offset)\n            }\n            lastOffset = offset + sample.size\n        }\n        val chunkOffsetsLong = LongArray(chunksOffsets.size)\n        for (a in chunksOffsets.indices) {\n            chunkOffsetsLong[a] = chunksOffsets[a]\n        }\n        val stco = StaticChunkOffsetBox()\n        stco.chunkOffsets = chunkOffsetsLong\n        stbl.addBox(stco)\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Mdat.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport com.coremedia.iso.BoxParser\nimport com.coremedia.iso.IsoFile\nimport com.coremedia.iso.IsoTypeWriter\nimport com.coremedia.iso.boxes.Box\nimport com.coremedia.iso.boxes.Container\nimport com.googlecode.mp4parser.DataSource\nimport java.nio.ByteBuffer\nimport java.nio.channels.WritableByteChannel\n\nclass Mdat : Box {\n\n    private lateinit var parent: Container\n    private var contentSize = (1024 * 1024 * 1024).toLong()\n    private var dataOffset: Long = 0\n\n    override fun getParent(): Container = parent\n\n    override fun setParent(parent: Container) {\n        this.parent = parent\n    }\n\n    override fun getSize(): Long = 16 + contentSize\n\n    override fun getOffset(): Long = dataOffset\n\n    fun setDataOffset(offset: Long) {\n        dataOffset = offset\n    }\n\n    fun setContentSize(contentSize: Long) {\n        this.contentSize = contentSize\n    }\n\n    fun getContentSize(): Long {\n        return contentSize\n    }\n\n    override fun getType(): String = \"mdat\"\n\n    private fun isSmallBox(contentSize: Long): Boolean = contentSize + 8 < 4294967296L\n\n    override fun getBox(writableByteChannel: WritableByteChannel) {\n        val bb = ByteBuffer.allocate(16)\n        val size = size\n        if (isSmallBox(size)) {\n            if (size >= 0 && size <= 1L shl 32) {\n                IsoTypeWriter.writeUInt32(bb, size)\n            } else {\n                // TODO(ABED): Investigate when this could happen.\n                IsoTypeWriter.writeUInt32(bb, 1)\n            }\n        } else {\n            IsoTypeWriter.writeUInt32(bb, 1)\n        }\n        bb.put(IsoFile.fourCCtoBytes(\"mdat\"))\n        if (isSmallBox(size)) {\n            bb.put(ByteArray(8))\n        } else {\n            IsoTypeWriter.writeUInt64(bb, if (size >= 0) size else 1)\n        }\n        bb.rewind()\n        writableByteChannel.write(bb)\n    }\n\n    override fun parse(\n        dataSource: DataSource?,\n        header: ByteBuffer?,\n        contentSize: Long,\n        boxParser: BoxParser?\n    ) {\n    }\n}"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Mp4Movie.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.media.MediaCodec\nimport android.media.MediaFormat\nimport com.googlecode.mp4parser.util.Matrix\nimport java.io.File\nimport java.util.*\n\nclass Mp4Movie {\n\n    private var matrix = Matrix.ROTATE_0\n    private val tracks = ArrayList<Track>()\n    private var cacheFile: File? = null\n\n    fun getMatrix(): Matrix? = matrix\n\n    fun setCacheFile(file: File) {\n        cacheFile = file\n    }\n\n    fun setRotation(angle: Int) {\n        when (angle) {\n            0 -> {\n                matrix = Matrix.ROTATE_0\n            }\n            90 -> {\n                matrix = Matrix.ROTATE_90\n            }\n            180 -> {\n                matrix = Matrix.ROTATE_180\n            }\n            270 -> {\n                matrix = Matrix.ROTATE_270\n            }\n        }\n    }\n\n    fun getTracks(): ArrayList<Track> = tracks\n\n    fun getCacheFile(): File? = cacheFile\n\n    fun addSample(trackIndex: Int, offset: Long, bufferInfo: MediaCodec.BufferInfo) {\n        if (trackIndex < 0 || trackIndex >= tracks.size) {\n            return\n        }\n        val track = tracks[trackIndex]\n        track.addSample(offset, bufferInfo)\n    }\n\n    fun addTrack(mediaFormat: MediaFormat, isAudio: Boolean): Int {\n        tracks.add(Track(tracks.size, mediaFormat, isAudio))\n        return tracks.size - 1\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/OutputSurface.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.graphics.SurfaceTexture\nimport android.graphics.SurfaceTexture.OnFrameAvailableListener\nimport android.view.Surface\n\nclass OutputSurface : OnFrameAvailableListener {\n\n    private var mSurfaceTexture: SurfaceTexture? = null\n    private var mSurface: Surface? = null\n    private val mFrameSyncObject = Object()\n    private var mFrameAvailable = false\n    private var mTextureRender: TextureRenderer? = null\n\n    /**\n     * Creates an OutputSurface using the current EGL context. This Surface will be\n     * passed to MediaCodec.configure().\n     */\n    init {\n        setup()\n    }\n\n    /**\n     * Creates instances of TextureRender and SurfaceTexture, and a Surface associated\n     * with the SurfaceTexture.\n     */\n    private fun setup() {\n        mTextureRender = TextureRenderer()\n        mTextureRender?.let {\n            it.surfaceCreated()\n\n            // Even if we don't access the SurfaceTexture after the constructor returns, we\n            // still need to keep a reference to it. The Surface doesn't retain a reference\n            // at the Java level, so if we don't either then the object can get GCed, which\n            // causes the native finalizer to run.\n            mSurfaceTexture = SurfaceTexture(it.getTextureId())\n            mSurfaceTexture?.let { surfaceTexture ->\n                surfaceTexture.setOnFrameAvailableListener(this)\n                mSurface = Surface(mSurfaceTexture)\n            }\n        }\n    }\n\n    /**\n     * Discards all resources held by this class, notably the EGL context.\n     */\n    fun release() {\n        mSurface?.release()\n\n        mTextureRender = null\n        mSurface = null\n        mSurfaceTexture = null\n    }\n\n    /**\n     * Returns the Surface that we draw onto.\n     */\n    fun getSurface(): Surface? = mSurface\n\n    /**\n     * Latches the next buffer into the texture.  Must be called from the thread that created\n     * the OutputSurface object, after the onFrameAvailable callback has signaled that new\n     * data is available.\n     */\n    fun awaitNewImage() {\n        val timeOutMS = 100\n        synchronized(mFrameSyncObject) {\n            while (!mFrameAvailable) {\n                try {\n                    // Wait for onFrameAvailable() to signal us.  Use a timeout to avoid\n                    // stalling the test if it doesn't arrive.\n                    mFrameSyncObject.wait(timeOutMS.toLong())\n                    if (!mFrameAvailable) {\n                        throw RuntimeException(\"Surface frame wait timed out\")\n                    }\n                } catch (ie: InterruptedException) {\n                    throw RuntimeException(ie)\n                }\n            }\n            mFrameAvailable = false\n        }\n        mTextureRender?.checkGlError(\"before updateTexImage\")\n        mSurfaceTexture?.updateTexImage()\n    }\n\n    /**\n     * Draws the data from SurfaceTexture onto the current EGL surface.\n     */\n    fun drawImage() {\n        mTextureRender?.drawFrame(mSurfaceTexture!!)\n    }\n\n    override fun onFrameAvailable(p0: SurfaceTexture?) {\n        synchronized(mFrameSyncObject) {\n            if (mFrameAvailable) {\n                throw RuntimeException(\"mFrameAvailable already set, frame could be dropped\")\n            }\n            mFrameAvailable = true\n            mFrameSyncObject.notifyAll()\n        }\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\ndata class Result(\n    val index: Int,\n    val success: Boolean,\n    val failureMessage: String?,\n    val size: Long = 0,\n    val path: String? = null,\n)\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Sample.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\ndata class Sample(var offset: Long, var size: Long)\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/TextureRenderer.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.graphics.SurfaceTexture\nimport android.opengl.GLES11Ext\nimport android.opengl.GLES20\nimport android.opengl.Matrix\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\nimport java.nio.FloatBuffer\n\nclass TextureRenderer {\n\n    private val floatSizeBytes = 4\n    private val triangleVerticesDataStrideBytes = 5 * floatSizeBytes\n    private val triangleVerticesDataPosOffset = 0\n    private val triangleVerticesDataUvOffset = 3\n    private var mTriangleVertices: FloatBuffer\n\n    private val vertexShader = \"\"\"uniform mat4 uMVPMatrix;\nuniform mat4 uSTMatrix;\nattribute vec4 aPosition;\nattribute vec4 aTextureCoord;\nvarying vec2 vTextureCoord;\nvoid main() {\n  gl_Position = uMVPMatrix * aPosition;\n  vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n}\n\"\"\"\n\n    private val fragmentShader = \"\"\"#extension GL_OES_EGL_image_external : require\nprecision mediump float;\nvarying vec2 vTextureCoord;\nuniform samplerExternalOES sTexture;\nvoid main() {\n  gl_FragColor = texture2D(sTexture, vTextureCoord);\n}\n\"\"\"\n\n    private val mMVPMatrix = FloatArray(16)\n    private val mSTMatrix = FloatArray(16)\n\n    private var mProgram = 0\n    private var mTextureID = -12345\n    private var muMVPMatrixHandle = 0\n    private var muSTMatrixHandle = 0\n    private var maPositionHandle = 0\n    private var maTextureHandle = 0\n\n    init {\n        val mTriangleVerticesData = floatArrayOf( // X, Y, Z, U, V\n            -1.0f, -1.0f, 0f, 0f, 0f,\n            1.0f, -1.0f, 0f, 1f, 0f,\n            -1.0f, 1.0f, 0f, 0f, 1f,\n            1.0f, 1.0f, 0f, 1f, 1f\n        )\n        mTriangleVertices = ByteBuffer.allocateDirect(\n            mTriangleVerticesData.size * floatSizeBytes\n        )\n            .order(ByteOrder.nativeOrder()).asFloatBuffer()\n        mTriangleVertices.put(mTriangleVerticesData).position(0)\n\n        Matrix.setIdentityM(mSTMatrix, 0)\n    }\n\n    fun getTextureId(): Int = mTextureID\n\n    fun drawFrame(st: SurfaceTexture) {\n        checkGlError(\"onDrawFrame start\")\n        st.getTransformMatrix(mSTMatrix)\n\n        GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f)\n        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT or GLES20.GL_COLOR_BUFFER_BIT)\n\n        GLES20.glUseProgram(mProgram)\n        checkGlError(\"glUseProgram\")\n\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID)\n\n        mTriangleVertices.position(triangleVerticesDataPosOffset)\n        GLES20.glVertexAttribPointer(\n            maPositionHandle, 3, GLES20.GL_FLOAT, false,\n            triangleVerticesDataStrideBytes, mTriangleVertices\n        )\n        checkGlError(\"glVertexAttribPointer maPosition\")\n        GLES20.glEnableVertexAttribArray(maPositionHandle)\n        checkGlError(\"glEnableVertexAttribArray maPositionHandle\")\n\n        mTriangleVertices.position(triangleVerticesDataUvOffset)\n        GLES20.glVertexAttribPointer(\n            maTextureHandle, 2, GLES20.GL_FLOAT, false,\n            triangleVerticesDataStrideBytes, mTriangleVertices\n        )\n        checkGlError(\"glVertexAttribPointer maTextureHandle\")\n        GLES20.glEnableVertexAttribArray(maTextureHandle)\n        checkGlError(\"glEnableVertexAttribArray maTextureHandle\")\n\n        Matrix.setIdentityM(mMVPMatrix, 0)\n        GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0)\n        GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0)\n\n        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)\n        checkGlError(\"glDrawArrays\")\n        GLES20.glFinish()\n    }\n\n    /**\n     * Initializes GL state.  Call this after the EGL surface has been created and made current.\n     */\n    fun surfaceCreated() {\n        mProgram = createProgram()\n        if (mProgram == 0) {\n            throw RuntimeException(\"failed creating program\")\n        }\n        maPositionHandle = GLES20.glGetAttribLocation(mProgram, \"aPosition\")\n        checkGlError(\"glGetAttribLocation aPosition\")\n        if (maPositionHandle == -1) {\n            throw RuntimeException(\"Could not get attrib location for aPosition\")\n        }\n        maTextureHandle = GLES20.glGetAttribLocation(mProgram, \"aTextureCoord\")\n        checkGlError(\"glGetAttribLocation aTextureCoord\")\n        if (maTextureHandle == -1) {\n            throw RuntimeException(\"Could not get attrib location for aTextureCoord\")\n        }\n\n        muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, \"uMVPMatrix\")\n        checkGlError(\"glGetUniformLocation uMVPMatrix\")\n        if (muMVPMatrixHandle == -1) {\n            throw RuntimeException(\"Could not get attrib location for uMVPMatrix\")\n        }\n\n        muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, \"uSTMatrix\")\n        checkGlError(\"glGetUniformLocation uSTMatrix\")\n        if (muSTMatrixHandle == -1) {\n            throw RuntimeException(\"Could not get attrib location for uSTMatrix\")\n        }\n\n        val textures = IntArray(1)\n        GLES20.glGenTextures(1, textures, 0)\n        mTextureID = textures[0]\n        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID)\n        checkGlError(\"glBindTexture mTextureID\")\n\n        GLES20.glTexParameterf(\n            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,\n            GLES20.GL_NEAREST.toFloat()\n        )\n        GLES20.glTexParameterf(\n            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,\n            GLES20.GL_LINEAR.toFloat()\n        )\n        GLES20.glTexParameteri(\n            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,\n            GLES20.GL_CLAMP_TO_EDGE\n        )\n        GLES20.glTexParameteri(\n            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,\n            GLES20.GL_CLAMP_TO_EDGE\n        )\n        checkGlError(\"glTexParameter\")\n    }\n\n    private fun loadShader(shaderType: Int, source: String): Int {\n        var shader = GLES20.glCreateShader(shaderType)\n        checkGlError(\"glCreateShader type=$shaderType\")\n        GLES20.glShaderSource(shader, source)\n        GLES20.glCompileShader(shader)\n        val compiled = IntArray(1)\n        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0)\n        if (compiled[0] == 0) {\n            GLES20.glDeleteShader(shader)\n            shader = 0\n        }\n        return shader\n    }\n\n    private fun createProgram(): Int {\n        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShader)\n        if (vertexShader == 0) {\n            return 0\n        }\n\n        val pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader)\n        if (pixelShader == 0) {\n            return 0\n        }\n\n        var program = GLES20.glCreateProgram()\n        checkGlError(\"glCreateProgram\")\n        if (program == 0) {\n            return 0\n        }\n        GLES20.glAttachShader(program, vertexShader)\n        checkGlError(\"glAttachShader\")\n        GLES20.glAttachShader(program, pixelShader)\n        checkGlError(\"glAttachShader\")\n        GLES20.glLinkProgram(program)\n        val linkStatus = IntArray(1)\n        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)\n        if (linkStatus[0] != GLES20.GL_TRUE) {\n            GLES20.glDeleteProgram(program)\n            program = 0\n        }\n        return program\n    }\n\n    fun checkGlError(op: String) {\n        var error: Int\n        if (GLES20.glGetError().also { error = it } != GLES20.GL_NO_ERROR) {\n            throw RuntimeException(\"$op: glError $error\")\n        }\n    }\n\n}"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Track.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.media.MediaCodec\nimport android.media.MediaCodecInfo\nimport android.media.MediaFormat\nimport com.coremedia.iso.boxes.SampleDescriptionBox\nimport com.coremedia.iso.boxes.sampleentry.AudioSampleEntry\nimport com.coremedia.iso.boxes.sampleentry.VisualSampleEntry\nimport com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox\nimport com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig\nimport com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor\nimport com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor\nimport com.googlecode.mp4parser.boxes.mp4.objectdescriptors.SLConfigDescriptor\nimport com.mp4parser.iso14496.part15.AvcConfigurationBox\nimport java.util.*\n\nclass Track(id: Int, format: MediaFormat, audio: Boolean) {\n\n    private var trackId: Long = 0\n    private val samples = ArrayList<Sample>()\n    private var duration: Long = 0\n    private var handler: String\n    private var sampleDescriptionBox: SampleDescriptionBox\n    private var syncSamples: LinkedList<Int>? = null\n    private var timeScale = 0\n    private val creationTime = Date()\n    private var height = 0\n    private var width = 0\n    private var volume = 0f\n    private val sampleDurations = ArrayList<Long>()\n    private val isAudio = audio\n    private var samplingFrequencyIndexMap: Map<Int, Int> = HashMap()\n    private var lastPresentationTimeUs: Long = 0\n    private var first = true\n\n    init {\n        samplingFrequencyIndexMap = mapOf(\n            96000 to 0x0,\n            88200 to 0x1,\n            64000 to 0x2,\n            48000 to 0x3,\n            44100 to 0x4,\n            32000 to 0x5,\n            24000 to 0x6,\n            22050 to 0x7,\n            16000 to 0x8,\n            12000 to 0x9,\n            11025 to 0xa,\n            8000 to 0xb,\n        )\n\n        trackId = id.toLong()\n        if (!isAudio) {\n            sampleDurations.add(3015.toLong())\n            duration = 3015\n            width = format.getInteger(MediaFormat.KEY_WIDTH)\n            height = format.getInteger(MediaFormat.KEY_HEIGHT)\n            timeScale = 90000\n            syncSamples = LinkedList()\n            handler = \"vide\"\n\n            sampleDescriptionBox = SampleDescriptionBox()\n            val mime = format.getString(MediaFormat.KEY_MIME)\n            if (mime == \"video/avc\") {\n                val visualSampleEntry =\n                    VisualSampleEntry(VisualSampleEntry.TYPE3).setup(width, height)\n\n                val avcConfigurationBox = AvcConfigurationBox()\n                if (format.getByteBuffer(\"csd-0\") != null) {\n                    val spsArray = ArrayList<ByteArray>()\n                    val spsBuff = format.getByteBuffer(\"csd-0\")\n                    spsBuff!!.position(4)\n\n                    val spsBytes = ByteArray(spsBuff.remaining())\n                    spsBuff[spsBytes]\n                    spsArray.add(spsBytes)\n\n                    val ppsArray = ArrayList<ByteArray>()\n                    val ppsBuff = format.getByteBuffer(\"csd-1\")\n                    ppsBuff?.let {\n                        it.position(4)\n\n                        val ppsBytes = ByteArray(it.remaining())\n                        it[ppsBytes]\n\n                        ppsArray.add(ppsBytes)\n                        avcConfigurationBox.sequenceParameterSets = spsArray\n                        avcConfigurationBox.pictureParameterSets = ppsArray\n                    }\n                }\n\n                if (format.containsKey(\"level\")) {\n                    when (format.getInteger(\"level\")) {\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel1 -> {\n                            avcConfigurationBox.avcLevelIndication = 1\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel2 -> {\n                            avcConfigurationBox.avcLevelIndication = 2\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel11 -> {\n                            avcConfigurationBox.avcLevelIndication = 11\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel12 -> {\n                            avcConfigurationBox.avcLevelIndication = 12\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel13 -> {\n                            avcConfigurationBox.avcLevelIndication = 13\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel21 -> {\n                            avcConfigurationBox.avcLevelIndication = 21\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel22 -> {\n                            avcConfigurationBox.avcLevelIndication = 22\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel3 -> {\n                            avcConfigurationBox.avcLevelIndication = 3\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel31 -> {\n                            avcConfigurationBox.avcLevelIndication = 31\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel32 -> {\n                            avcConfigurationBox.avcLevelIndication = 32\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel4 -> {\n                            avcConfigurationBox.avcLevelIndication = 4\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel41 -> {\n                            avcConfigurationBox.avcLevelIndication = 41\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel42 -> {\n                            avcConfigurationBox.avcLevelIndication = 42\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel5 -> {\n                            avcConfigurationBox.avcLevelIndication = 5\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel51 -> {\n                            avcConfigurationBox.avcLevelIndication = 51\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel52 -> {\n                            avcConfigurationBox.avcLevelIndication = 52\n                        }\n                        MediaCodecInfo.CodecProfileLevel.AVCLevel1b -> {\n                            avcConfigurationBox.avcLevelIndication = 0x1b\n                        }\n                        else -> avcConfigurationBox.avcLevelIndication = 13\n                    }\n                } else {\n                    avcConfigurationBox.avcLevelIndication = 13\n                }\n\n                avcConfigurationBox.avcProfileIndication = 100\n                avcConfigurationBox.bitDepthLumaMinus8 = -1\n                avcConfigurationBox.bitDepthChromaMinus8 = -1\n                avcConfigurationBox.chromaFormat = -1\n                avcConfigurationBox.configurationVersion = 1\n                avcConfigurationBox.lengthSizeMinusOne = 3\n                avcConfigurationBox.profileCompatibility = 0\n\n                visualSampleEntry.addBox(avcConfigurationBox)\n                sampleDescriptionBox.addBox(visualSampleEntry)\n\n            } else if (mime == \"video/mp4v\") {\n                val visualSampleEntry =\n                    VisualSampleEntry(VisualSampleEntry.TYPE1).setup(width, height)\n                sampleDescriptionBox.addBox(visualSampleEntry)\n            }\n        } else {\n            sampleDurations.add(1024.toLong())\n            duration = 1024\n            volume = 1f\n            timeScale = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)\n            handler = \"soun\"\n            sampleDescriptionBox = SampleDescriptionBox()\n\n            val audioSampleEntry = AudioSampleEntry(AudioSampleEntry.TYPE3).setup(format)\n\n            val esds = ESDescriptorBox()\n\n            val descriptor = ESDescriptor()\n            descriptor.esId = 0\n\n            val slConfigDescriptor = SLConfigDescriptor()\n            slConfigDescriptor.predefined = 2\n            descriptor.slConfigDescriptor = slConfigDescriptor\n\n            val decoderConfigDescriptor = DecoderConfigDescriptor().setup()\n\n            val audioSpecificConfig = AudioSpecificConfig()\n            audioSpecificConfig.setAudioObjectType(2)\n            audioSpecificConfig.setSamplingFrequencyIndex(\n                samplingFrequencyIndexMap[audioSampleEntry.sampleRate.toInt()]!!\n            )\n            audioSpecificConfig.setChannelConfiguration(audioSampleEntry.channelCount)\n            decoderConfigDescriptor.audioSpecificInfo = audioSpecificConfig\n            descriptor.decoderConfigDescriptor = decoderConfigDescriptor\n\n            val data = descriptor.serialize()\n            esds.esDescriptor = descriptor\n            esds.data = data\n            audioSampleEntry.addBox(esds)\n            sampleDescriptionBox.addBox(audioSampleEntry)\n        }\n    }\n\n    fun getTrackId(): Long = trackId\n\n    fun addSample(offset: Long, bufferInfo: MediaCodec.BufferInfo) {\n        val isSyncFrame = !isAudio && bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0\n\n        samples.add(Sample(offset, bufferInfo.size.toLong()))\n\n        if (syncSamples != null && isSyncFrame) {\n            syncSamples?.add(samples.size)\n        }\n        var delta = bufferInfo.presentationTimeUs - lastPresentationTimeUs\n        lastPresentationTimeUs = bufferInfo.presentationTimeUs\n        delta = (delta * timeScale + 500000L) / 1000000L\n        if (!first) {\n            sampleDurations.add(sampleDurations.size - 1, delta)\n            duration += delta\n        }\n        first = false\n    }\n\n    fun getSamples(): ArrayList<Sample> = samples\n\n    fun getDuration(): Long = duration\n\n    fun getHandler(): String = handler\n\n    fun getSampleDescriptionBox(): SampleDescriptionBox = sampleDescriptionBox\n\n    fun getSyncSamples(): LongArray? {\n        if (syncSamples == null || syncSamples!!.isEmpty()) {\n            return null\n        }\n        val returns = LongArray(syncSamples!!.size)\n        for (i in syncSamples!!.indices) {\n            returns[i] = syncSamples!![i].toLong()\n        }\n        return returns\n    }\n\n    fun getTimeScale(): Int = timeScale\n\n    fun getCreationTime(): Date = creationTime\n\n    fun getWidth(): Int = width\n\n    fun getHeight(): Int = height\n\n    fun getVolume(): Float = volume\n\n    fun getSampleDurations(): ArrayList<Long> = sampleDurations\n\n    fun isAudio(): Boolean = isAudio\n\n    private fun DecoderConfigDescriptor.setup(): DecoderConfigDescriptor = apply {\n        objectTypeIndication = 0x40\n        streamType = 5\n        bufferSizeDB = 1536\n        maxBitRate = 96000\n        avgBitRate = 96000\n    }\n\n    private fun VisualSampleEntry.setup(w: Int, h: Int): VisualSampleEntry = apply {\n        dataReferenceIndex = 1\n        depth = 24\n        frameCount = 1\n        horizresolution = 72.0\n        vertresolution = 72.0\n        width = w\n        height = h\n        compressorname = \"AVC Coding\"\n    }\n\n    private fun AudioSampleEntry.setup(format: MediaFormat): AudioSampleEntry = apply {\n        channelCount =\n            if (format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1) 2 else format.getInteger(\n                MediaFormat.KEY_CHANNEL_COUNT\n            )\n        sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE).toLong()\n        dataReferenceIndex = 1\n        sampleSize = 16\n    }\n}\n"
  },
  {
    "path": "lightcompressor/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">LightCompressorLibrary</string>\n</resources>\n"
  },
  {
    "path": "lightcompressor/src/test/java/com/abedelazizshe/lightcompressorlibrary/ExampleUnitTest.kt",
    "content": "package com.abedelazizshe.lightcompressorlibrary\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}\n"
  },
  {
    "path": "settings.gradle",
    "content": "dependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n    }\n}\ninclude ':app', ':lightcompressor'\nrootProject.name='VideoCompressor'"
  }
]