master e858e91e0c36 cached
71 files
189.6 KB
46.0k tokens
1 requests
Download .txt
Showing preview only (211K chars total). Download the full file or copy to clipboard to get everything.
Repository: AbedElazizShe/LightCompressor
Branch: master
Commit: e858e91e0c36
Files: 71
Total size: 189.6 KB

Directory structure:
gitextract_wb5gjwc9/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .idea/
│   ├── .name
│   ├── codeStyles/
│   │   ├── Project.xml
│   │   └── codeStyleConfig.xml
│   ├── compiler.xml
│   ├── dictionaries/
│   │   └── abdsh.xml
│   ├── gradle.xml
│   ├── inspectionProfiles/
│   │   └── Project_Default.xml
│   ├── jarRepositories.xml
│   ├── kotlinc.xml
│   ├── misc.xml
│   └── vcs.xml
├── LICENSE
├── README.md
├── app/
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── abedelazizshe/
│       │               └── lightcompressor/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── abedelazizshe/
│       │   │           └── lightcompressor/
│       │   │               ├── MainActivity.kt
│       │   │               ├── RecyclerViewAdapter.kt
│       │   │               ├── Utils.kt
│       │   │               ├── VideoDetailsModel.kt
│       │   │               └── VideoPlayerActivity.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_launcher_background.xml
│       │       │   ├── ic_play_white_24dp.xml
│       │       │   └── ic_video_library_white_24dp.xml
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_video_player.xml
│       │       │   ├── content_main.xml
│       │       │   └── recycler_view_item.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       └── xml/
│       │           └── media_capabilities.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── abedelazizshe/
│                       └── lightcompressor/
│                           └── ExampleUnitTest.kt
├── build.gradle
├── gradle/
│   └── wrapper/
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── lightcompressor/
│   ├── .idea/
│   │   └── .gitignore
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── abedelazizshe/
│       │               └── lightcompressorlibrary/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── abedelazizshe/
│       │   │           └── lightcompressorlibrary/
│       │   │               ├── CompressionInterface.kt
│       │   │               ├── VideoCompressor.kt
│       │   │               ├── compressor/
│       │   │               │   └── Compressor.kt
│       │   │               ├── config/
│       │   │               │   ├── Configuration.kt
│       │   │               │   └── VideoResizer.kt
│       │   │               ├── data/
│       │   │               │   └── Atoms.kt
│       │   │               ├── utils/
│       │   │               │   ├── CompressorUtils.kt
│       │   │               │   ├── FileUtils.kt
│       │   │               │   ├── NumbersUtils.kt
│       │   │               │   └── StreamableVideo.kt
│       │   │               └── video/
│       │   │                   ├── InputSurface.kt
│       │   │                   ├── MP4Builder.kt
│       │   │                   ├── Mdat.kt
│       │   │                   ├── Mp4Movie.kt
│       │   │                   ├── OutputSurface.kt
│       │   │                   ├── Result.kt
│       │   │                   ├── Sample.kt
│       │   │                   ├── TextureRenderer.kt
│       │   │                   └── Track.kt
│       │   └── res/
│       │       └── values/
│       │           └── strings.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── abedelazizshe/
│                       └── lightcompressorlibrary/
│                           └── ExampleUnitTest.kt
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://www.paypal.com/paypalme/abedelazizshehadeh1/USD5']


================================================
FILE: .gitignore
================================================
.classpath
.DS_Store
.externalNativeBuild
.project
.gradle
.mtj.tmp
.vscode
.settings
.cxx

/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml

local.properties
maven-repository
mvn-clone
build
captures
gen
out
target
tmpmob

*.class
*.txt
*.ear
*.iml
*.jar
*.keystore
*.log
*.nar
*.rar
*.tar.gz
*.war
*.zip

================================================
FILE: .idea/.name
================================================
VideoCompressor

================================================
FILE: .idea/codeStyles/Project.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <code_scheme name="Project" version="173">
    <JetCodeStyleSettings>
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </JetCodeStyleSettings>
    <codeStyleSettings language="XML">
      <indentOptions>
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
      </indentOptions>
      <arrangement>
        <rules>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>xmlns:android</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>xmlns:.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:id</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:name</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>name</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>style</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
              <order>ANDROID_ATTRIBUTE_ORDER</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>.*</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
        </rules>
      </arrangement>
    </codeStyleSettings>
    <codeStyleSettings language="kotlin">
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </codeStyleSettings>
  </code_scheme>
</component>

================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
  </state>
</component>

================================================
FILE: .idea/compiler.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="CompilerConfiguration">
    <bytecodeTargetLevel target="17" />
  </component>
</project>

================================================
FILE: .idea/dictionaries/abdsh.xml
================================================
<component name="ProjectDictionaryState">
  <dictionary name="abdsh">
    <words>
      <w>ftyp</w>
      <w>mdat</w>
      <w>moov</w>
      <w>muxer</w>
    </words>
  </dictionary>
</component>

================================================
FILE: .idea/gradle.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="GradleMigrationSettings" migrationVersion="1" />
  <component name="GradleSettings">
    <option name="linkedExternalProjectsSettings">
      <GradleProjectSettings>
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
        <option name="modules">
          <set>
            <option value="$PROJECT_DIR$" />
            <option value="$PROJECT_DIR$/app" />
            <option value="$PROJECT_DIR$/lightcompressor" />
          </set>
        </option>
        <option name="resolveExternalAnnotations" value="false" />
      </GradleProjectSettings>
    </option>
  </component>
</project>

================================================
FILE: .idea/inspectionProfiles/Project_Default.xml
================================================
<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Project Default" />
    <inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
      <option name="TOP_LEVEL_CLASS_OPTIONS">
        <value>
          <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
          <option name="REQUIRED_TAGS" value="" />
        </value>
      </option>
      <option name="INNER_CLASS_OPTIONS">
        <value>
          <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
          <option name="REQUIRED_TAGS" value="" />
        </value>
      </option>
      <option name="METHOD_OPTIONS">
        <value>
          <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
          <option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
        </value>
      </option>
      <option name="FIELD_OPTIONS">
        <value>
          <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
          <option name="REQUIRED_TAGS" value="" />
        </value>
      </option>
      <option name="IGNORE_DEPRECATED" value="false" />
      <option name="IGNORE_JAVADOC_PERIOD" value="true" />
      <option name="IGNORE_DUPLICATED_THROWS" value="false" />
      <option name="IGNORE_POINT_TO_ITSELF" value="false" />
      <option name="myAdditionalJavadocTags" value="date" />
    </inspection_tool>
  </profile>
</component>

================================================
FILE: .idea/jarRepositories.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="RemoteRepositoriesConfiguration">
    <remote-repository>
      <option name="id" value="central" />
      <option name="name" value="Maven Central repository" />
      <option name="url" value="https://repo1.maven.org/maven2" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="jboss.community" />
      <option name="name" value="JBoss Community repository" />
      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="BintrayJCenter" />
      <option name="name" value="BintrayJCenter" />
      <option name="url" value="https://jcenter.bintray.com/" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="maven" />
      <option name="name" value="maven" />
      <option name="url" value="https://jitpack.io" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="Google" />
      <option name="name" value="Google" />
      <option name="url" value="https://dl.google.com/dl/android/maven2/" />
    </remote-repository>
    <remote-repository>
      <option name="id" value="MavenRepo" />
      <option name="name" value="MavenRepo" />
      <option name="url" value="https://repo.maven.apache.org/maven2/" />
    </remote-repository>
  </component>
</project>

================================================
FILE: .idea/kotlinc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="KotlinJpsPluginSettings">
    <option name="version" value="1.8.21" />
  </component>
</project>

================================================
FILE: .idea/misc.xml
================================================
<project version="4">
  <component name="CMakeSettings">
    <configurations>
      <configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
    </configurations>
  </component>
  <component name="DesignSurface">
    <option name="filePathToZoomLevelMap">
      <map>
        <entry key="..\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/activity_main.xml" value="0.17831813576494426" />
        <entry key="..\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/activity_video_player.xml" value="0.3641304347826087" />
        <entry key="..\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/content_main.xml" value="0.3641304347826087" />
        <entry key="..\:/AndroidStudioProjects/VideoCompressor/app/src/main/res/layout/recycler_view_item.xml" value="0.3641304347826087" />
      </map>
    </option>
  </component>
  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
    <output url="file://$PROJECT_DIR$/build/classes" />
  </component>
  <component name="ProjectType">
    <option name="id" value="Android" />
  </component>
</project>

================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
[![JitPack](https://jitpack.io/v/AbedElazizShe/LightCompressor.svg)](https://jitpack.io/#AbedElazizShe/LightCompressor)


# LightCompressor

LightCompressor can now be used in Flutter through [light_compressor](https://pub.dev/packages/light_compressor) plugin.

A 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
seconds that determines the video and audio files’ size and quality). It is based on Telegram for Android project.

The 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.

I 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.

**LightCompressor is now available in iOS**, have a look at [LightCompressor_iOS](https://github.com/AbedElazizShe/LightCompressor_iOS).

# Change Logs

## What's new in 1.3.3

- Thanks to [LiewJunTung](https://github.com/AbedElazizShe/LightCompressor/pull/181) for improving the error handling.
- Thanks to [CristianMG](https://github.com/AbedElazizShe/LightCompressor/pull/182) for improving the storage configuration and making the library testable.
- Thanks to [dan3988](https://github.com/AbedElazizShe/LightCompressor/pull/188) for replacing video size with resizer which made using the library way more flexible.
- Thanks to [imSzukala](https://github.com/AbedElazizShe/LightCompressor/pull/191) for changing min supported api to 21.
- Thanks to [josebraz](https://github.com/AbedElazizShe/LightCompressor/pull/192) for improving codec profile approach.
- Thanks to [ryccoatika](https://github.com/AbedElazizShe/LightCompressor/pull/198) for improving exception handling for the coroutines.


## How it works
When 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;
* Bitrate: 2mbps

You can as well pass custom resizer and videoBitrate values if you don't want the library to auto-generate the values for you.

These 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.

## Demo
![Demo](/pictures/demo.gif)

Usage
--------
To 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.

**API < 29**

```xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28"
    tools:ignore="ScopedStorage" />
```

**API >= 29**

```xml
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32"/>
```

**API >= 33**

```xml
 <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
```

```kotlin

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
     // request READ_MEDIA_VIDEO run-time permission
 } else {
     // request WRITE_EXTERNAL_STORAGE run-time permission
 }
```

And import the following dependencies to use kotlin coroutines

### Groovy

```groovy
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.coroutines}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.coroutines}"
```

Then just call [VideoCompressor.start()] and pass **context**, **uris**, **isStreamable**, **configureWith**, and either **sharedStorageConfiguration OR appSpecificStorageConfiguration**.

The method has a callback for 5 functions;
1) OnStart - called when compression started
2) OnSuccess - called when compression completed with no errors/exceptions
3) OnFailure - called when an exception occurred or video bitrate and size are below the minimum required for compression.
4) OnProgress - called with progress new value
5) OnCancelled - called when the job is cancelled

### Important Notes:

- 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
or retrieve information about the original uri/file.
- The source video must be provided as a list of content uris.
- OnSuccess returns the path of the stored video.
- If you want an output video that is optimised to be streamed, ensure you pass [isStreamable] flag is true.

### Configuration values

- 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)

- isMinBitrateCheckEnabled: this means, don't compress if bitrate is less than 2mbps

- videoBitrateInMbps: any custom bitrate value in Mbps.

- disableAudio: true/false to generate a video without audio. False by default.

- resizer: Function to resize the video dimensions. `VideoResizer.auto` by default.


## The StorageConfiguration is an interface which indicate library where will be saved the File

#### Library provides some behaviors defined to be more easy to use, specified the next

### AppSpecificStorageConfiguration Configuration values

- subFolderName: a subfolder name created in app's specific storage. 

### SharedStorageConfiguration Configuration values

- saveAt: the directory where the video should be saved in. Must be one of the following; [SaveLocation.pictures], [SaveLocation.movies], or [SaveLocation.downloads].
- subFolderName: a subfolder name created in shared storage. 

### CacheStorageConfiguration
- 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)

### Fully custom configuration
- 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

```kotlin
class FullyCustomizedStorageConfiguration(
) : StorageConfiguration {
    override fun createFileToSave(
        context: Context,
        videoFile: File,
        fileName: String,
        shouldSave: Boolean
    ): File = ??? What you need 
}

```

To cancel the compression job, just call [VideoCompressor.cancel()]

### Kotlin

```kotlin
VideoCompressor.start(
   context = applicationContext, // => This is required
   uris = List<Uri>, // => Source can be provided as content uris
   isStreamable = false, 
   // THIS STORAGE
   storageConfiguration = SharedStorageConfiguration(
       saveAt = SaveLocation.movies, // => default is movies
       subFolderName = "my-videos" // => optional
   )
   configureWith = Configuration(
      videoNames = listOf<String>(), /*list of video names, the size should be similar to the passed uris*/
      quality = VideoQuality.MEDIUM,
      isMinBitrateCheckEnabled = true,
      videoBitrateInMbps = 5, /*Int, ignore, or null*/
      disableAudio = false, /*Boolean, or ignore*/
      resizer = VideoResizer.matchSize(360, 480) /*VideoResizer, ignore, or null*/
   ),
   listener = object : CompressionListener {
       override fun onProgress(index: Int, percent: Float) {
          // Update UI with progress value
          runOnUiThread {
          }
       }

       override fun onStart(index: Int) {
          // Compression start
       }

       override fun onSuccess(index: Int, size: Long, path: String?) {
         // On Compression success
       }

       override fun onFailure(index: Int, failureMessage: String) {
         // On Failure
       }

       override fun onCancelled(index: Int) {
         // On Cancelled
       }

   }
)
```

## Common issues

- Sending the video to whatsapp when disableAudio = false, won't succeed [ at least for now ]. Whatsapp's own compression does not work with
LightCompressor library. You can send the video as document.

- 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
from within the main thread. Have a look at the example code above for more information.

## Reporting issues
To report an issue, please specify the following:
- Device name
- Android version

## Compatibility
Minimum Android SDK: LightCompressor requires a minimum API level of 21.

## How to add to your project?
#### Gradle

Ensure Kotlin version is `1.8.21`

Include this in your Project-level build.gradle file:

### Groovy

```groovy
allprojects {
    repositories {
        .
        .
        .
        maven { url 'https://jitpack.io' }
    }
}
```

Include this in your Module-level build.gradle file:

### Groovy

```groovy
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.3'
```

If you're facing problems with the setup, edit settings.gradle by adding this at the beginning of the file:

```
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}
```

## Getting help
For questions, suggestions, or anything else, email elaziz.shehadeh(at)gmail.com

## Credits
[Telegram](https://github.com/DrKLO/Telegram) for Android.


================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 33
    defaultConfig {
        applicationId "com.abedelazizshe.lightcompressor"
        minSdkVersion 21
        targetSdkVersion 33
        versionCode 1
        versionName "1.0.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8
    }

    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':lightcompressor')

    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.21"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"

    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.core:core-ktx:1.10.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation "com.google.android.material:material:1.9.0"
    implementation "com.github.bumptech.glide:glide:4.12.0"
    kapt 'com.github.bumptech.glide:compiler:4.12.0'
    implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
    implementation 'androidx.recyclerview:recyclerview:1.3.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'

    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.5"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}


================================================
FILE: app/src/androidTest/java/com/abedelazizshe/lightcompressor/ExampleInstrumentedTest.kt
================================================
package com.abedelazizshe.lightcompressor

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.abedelazizshe.lightcompressor", appContext.packageName)
    }
}


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.abedelazizshe.lightcompressor">

    <queries>
        <intent>
            <action android:name="android.media.action.VIDEO_CAPTURE" />
        </intent>
    </queries>

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32"/>
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28"
        tools:ignore="ScopedStorage" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

    <uses-feature
        android:name="android.hardware.camera"
        android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="AllowBackup,GoogleAppIndexingWarning"
        >

        <activity android:name=".VideoPlayerActivity" />
        <activity
            android:name=".MainActivity"
            android:theme="@style/AppTheme.NoActionBar"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <property
            android:name="android.content.MEDIA_CAPABILITIES"
            android:resource="@xml/media_capabilities" />
    </application>

</manifest>

================================================
FILE: app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt
================================================
package com.abedelazizshe.lightcompressor

import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.abedelazizshe.lightcompressor.databinding.ActivityMainBinding
import com.abedelazizshe.lightcompressorlibrary.CompressionListener
import com.abedelazizshe.lightcompressorlibrary.VideoCompressor
import com.abedelazizshe.lightcompressorlibrary.VideoQuality
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
import com.abedelazizshe.lightcompressorlibrary.config.VideoResizer
import com.abedelazizshe.lightcompressorlibrary.config.SaveLocation
import com.abedelazizshe.lightcompressorlibrary.config.SharedStorageConfiguration
import kotlinx.coroutines.launch

/**
 * Created by AbedElaziz Shehadeh on 26 Jan, 2020
 * elaziz.shehadeh@gmail.com
 */
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    companion object {
        const val REQUEST_SELECT_VIDEO = 0
        const val REQUEST_CAPTURE_VIDEO = 1
    }

    private val uris = mutableListOf<Uri>()
    private val data = mutableListOf<VideoDetailsModel>()
    private lateinit var adapter: RecyclerViewAdapter


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setReadStoragePermission()

        binding.pickVideo.setOnClickListener {
            pickVideo()
        }

        binding.recordVideo.setOnClickListener {
            dispatchTakeVideoIntent()
        }

        binding.cancel.setOnClickListener {
            VideoCompressor.cancel()
        }

        val recyclerview = findViewById<RecyclerView>(R.id.recyclerview)
        recyclerview.layoutManager = LinearLayoutManager(this)
        adapter = RecyclerViewAdapter(applicationContext, data)
        recyclerview.adapter = adapter
    }

    //Pick a video file from device
    private fun pickVideo() {
        val intent = Intent()
        intent.apply {
            type = "video/*"
            action = Intent.ACTION_PICK
        }
        intent.putExtra(
            Intent.EXTRA_ALLOW_MULTIPLE,
            true
        )
        startActivityForResult(Intent.createChooser(intent, "Select video"), REQUEST_SELECT_VIDEO)
    }

    private fun dispatchTakeVideoIntent() {
        Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
            takeVideoIntent.resolveActivity(packageManager)?.also {
                startActivityForResult(takeVideoIntent, REQUEST_CAPTURE_VIDEO)
            }
        }
    }

    @SuppressLint("SetTextI18n")
    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {

        reset()

        if (resultCode == Activity.RESULT_OK)
            if (requestCode == REQUEST_SELECT_VIDEO || requestCode == REQUEST_CAPTURE_VIDEO) {
                handleResult(intent)
            }

        super.onActivityResult(requestCode, resultCode, intent)
    }

    private fun handleResult(data: Intent?) {
        val clipData: ClipData? = data?.clipData
        if (clipData != null) {
            for (i in 0 until clipData.itemCount) {
                val videoItem = clipData.getItemAt(i)
                uris.add(videoItem.uri)
            }
            processVideo()
        } else if (data != null && data.data != null) {
            val uri = data.data
            uris.add(uri!!)
            processVideo()
        }
    }

    private fun reset() {
        uris.clear()
        binding.mainContents.visibility = View.GONE
        data.clear()
        adapter.notifyDataSetChanged()
    }

    private fun setReadStoragePermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            if (ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.READ_MEDIA_VIDEO,
                ) != PackageManager.PERMISSION_GRANTED
            ) {

                if (!ActivityCompat.shouldShowRequestPermissionRationale(
                        this,
                        Manifest.permission.READ_MEDIA_VIDEO
                    )
                ) {
                    ActivityCompat.requestPermissions(
                        this,
                        arrayOf(Manifest.permission.READ_MEDIA_VIDEO),
                        1
                    )
                }
            }
        } else {
            if (ContextCompat.checkSelfPermission(
                    this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                ) != PackageManager.PERMISSION_GRANTED
            ) {

                if (!ActivityCompat.shouldShowRequestPermissionRationale(
                        this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                    )
                ) {
                    ActivityCompat.requestPermissions(
                        this,
                        arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                        1
                    )
                }
            }
        }
    }

    @SuppressLint("SetTextI18n")
    private fun processVideo() {
        binding.mainContents.visibility = View.VISIBLE

        lifecycleScope.launch {
            VideoCompressor.start(
                context = applicationContext,
                uris,
                isStreamable = false,
                storageConfiguration = SharedStorageConfiguration(
                    saveAt = SaveLocation.movies,
                    subFolderName = "my-demo-videos"
                ),
                configureWith = Configuration(
                    quality = VideoQuality.LOW,
                    videoNames = uris.map { uri -> uri.pathSegments.last() },
                    isMinBitrateCheckEnabled = false,
                    resizer = VideoResizer.limitSize(1280.0)
                ),
                listener = object : CompressionListener {
                    override fun onProgress(index: Int, percent: Float) {
                        //Update UI
                        if (percent <= 100)
                            runOnUiThread {
                                data[index] = VideoDetailsModel(
                                    "",
                                    uris[index],
                                    "",
                                    percent
                                )
                                adapter.notifyDataSetChanged()
                            }
                    }

                    override fun onStart(index: Int) {
                        data.add(
                            index,
                            VideoDetailsModel("", uris[index], "")
                        )
                        runOnUiThread {
                            adapter.notifyDataSetChanged()
                        }

                    }

                    override fun onSuccess(index: Int, size: Long, path: String?) {
                        data[index] = VideoDetailsModel(
                            path,
                            uris[index],
                            getFileSize(size),
                            100F
                        )
                        runOnUiThread {
                            adapter.notifyDataSetChanged()
                        }
                    }

                    override fun onFailure(index: Int, failureMessage: String) {
                        Log.wtf("failureMessage", failureMessage)
                    }

                    override fun onCancelled(index: Int) {
                        Log.wtf("TAG", "compression has been cancelled")
                        // make UI changes, cleanup, etc
                    }
                },
            )
        }
    }
}


================================================
FILE: app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt
================================================
package com.abedelazizshe.lightcompressor

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide

class RecyclerViewAdapter(private val context: Context, private val list: List<VideoDetailsModel>) :
    RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.recycler_view_item, parent, false)

        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        val itemsViewModel = list[position]
        val newSize = "Size after compression: ${itemsViewModel.newSize}"
        val progress = "${itemsViewModel.progress.toLong()}%"

        if (itemsViewModel.progress > 0 && itemsViewModel.progress < 100) {
            holder.progress.visibility = View.VISIBLE
            holder.progress.text = progress

            holder.progressBar.visibility = View.VISIBLE
            holder.progressBar.progress = itemsViewModel.progress.toInt()
        } else {
            holder.progress.visibility = View.GONE
            holder.progressBar.visibility = View.GONE
        }

        if (itemsViewModel.newSize.isNotBlank()) {
            holder.newSize.text = newSize
            holder.newSize.visibility = View.VISIBLE
        } else {
            holder.newSize.visibility = View.GONE
        }

        Glide.with(context).load(itemsViewModel.uri).into(holder.videoImage)

        holder.itemView.setOnClickListener {
            VideoPlayerActivity.start(
                it.context,
                itemsViewModel.playableVideoPath
            )
        }
    }

    override fun getItemCount(): Int {
        return list.size
    }

    class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
        val videoImage: ImageView = itemView.findViewById(R.id.videoImage)
        val newSize: TextView = itemView.findViewById(R.id.newSize)
        val progress: TextView = itemView.findViewById(R.id.progress)
        val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
    }
}


================================================
FILE: app/src/main/java/com/abedelazizshe/lightcompressor/Utils.kt
================================================
package com.abedelazizshe.lightcompressor

import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import java.io.*
import java.text.DecimalFormat
import kotlin.math.log10
import kotlin.math.pow

fun getMediaPath(context: Context, uri: Uri): String {

    val resolver = context.contentResolver
    val projection = arrayOf(MediaStore.Video.Media.DATA)
    var cursor: Cursor? = null
    try {
        cursor = resolver.query(uri, projection, null, null, null)
        return if (cursor != null) {
            val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
            cursor.moveToFirst()
            cursor.getString(columnIndex)

        } else ""

    } catch (e: Exception) {
        resolver.let {
            val filePath = (context.applicationInfo.dataDir + File.separator
                    + System.currentTimeMillis())
            val file = File(filePath)

            resolver.openInputStream(uri)?.use { inputStream ->
                FileOutputStream(file).use { outputStream ->
                    val buf = ByteArray(4096)
                    var len: Int
                    while (inputStream.read(buf).also { len = it } > 0) outputStream.write(
                        buf,
                        0,
                        len
                    )
                }
            }
            return file.absolutePath
        }
    } finally {
        cursor?.close()
    }
}

fun getFileSize(size: Long): String {
    if (size <= 0)
        return "0"

    val units = arrayOf("B", "KB", "MB", "GB", "TB")
    val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt()

    return DecimalFormat("#,##0.#").format(
        size / 1024.0.pow(digitGroups.toDouble())
    ) + " " + units[digitGroups]
}

//The following methods can be alternative to [getMediaPath].
// todo(abed): remove [getPathFromUri], [getVideoExtension], and [copy]
fun getPathFromUri(context: Context, uri: Uri): String {
    var file: File? = null
    var inputStream: InputStream? = null
    var outputStream: OutputStream? = null
    var success = false
    try {
        val extension: String = getVideoExtension(uri)
        inputStream = context.contentResolver.openInputStream(uri)
        file = File.createTempFile("compressor", extension, context.cacheDir)
        file.deleteOnExit()
        outputStream = FileOutputStream(file)
        if (inputStream != null) {
            copy(inputStream, outputStream)
            success = true
        }
    } catch (ignored: IOException) {
    } finally {
        try {
            inputStream?.close()
        } catch (ignored: IOException) {
        }
        try {
            outputStream?.close()
        } catch (ignored: IOException) {
            // If closing the output stream fails, we cannot be sure that the
            // target file was written in full. Flushing the stream merely moves
            // the bytes into the OS, not necessarily to the file.
            success = false
        }
    }
    return if (success) file!!.path else ""
}

/** @return extension of video with dot, or default .mp4 if it none.
 */
private fun getVideoExtension(uriVideo: Uri): String {
    var extension: String? = null
    try {
        val imagePath = uriVideo.path
        if (imagePath != null && imagePath.lastIndexOf(".") != -1) {
            extension = imagePath.substring(imagePath.lastIndexOf(".") + 1)
        }
    } catch (e: Exception) {
        extension = null
    }
    if (extension == null || extension.isEmpty()) {
        //default extension for matches the previous behavior of the plugin
        extension = "mp4"
    }
    return ".$extension"
}

private fun copy(`in`: InputStream, out: OutputStream) {
    val buffer = ByteArray(4 * 1024)
    var bytesRead: Int
    while (`in`.read(buffer).also { bytesRead = it } != -1) {
        out.write(buffer, 0, bytesRead)
    }
    out.flush()
}


================================================
FILE: app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt
================================================
package com.abedelazizshe.lightcompressor

import android.net.Uri

data class VideoDetailsModel(
    val playableVideoPath: String?,
    val uri: Uri,
    val newSize: String,
    val progress: Float = 0F
)


================================================
FILE: app/src/main/java/com/abedelazizshe/lightcompressor/VideoPlayerActivity.kt
================================================
package com.abedelazizshe.lightcompressor

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.abedelazizshe.lightcompressor.databinding.ActivityVideoPlayerBinding
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.DefaultRenderersFactory
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
import java.io.File

/**
 * Created by AbedElaziz Shehadeh on 26 Jan, 2020
 * elaziz.shehadeh@gmail.com
 */
class VideoPlayerActivity : AppCompatActivity() {

    private lateinit var binding: ActivityVideoPlayerBinding

    private lateinit var exoPlayer: SimpleExoPlayer
    private var uri = ""

    companion object {
        fun start(context: Context, uri: String?) {
            val intent = Intent(context, VideoPlayerActivity::class.java)
                .putExtra("uri", uri)
            context.startActivity(intent)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
        setContentView(binding.root)

        intent?.extras?.let {
            uri = it.getString("uri", "")
        }
        initializePlayer()
    }

    private fun initializePlayer() {

        val trackSelector = DefaultTrackSelector(this)
        val loadControl = DefaultLoadControl()
        val rendererFactory = DefaultRenderersFactory(this)

        exoPlayer = SimpleExoPlayer.Builder(this, rendererFactory)
            .setLoadControl(loadControl)
            .setTrackSelector(trackSelector)
            .build()
    }

    private fun play(uri: Uri) {

        val userAgent = Util.getUserAgent(this, getString(R.string.app_name))
        val mediaSource = ProgressiveMediaSource
            .Factory(DefaultDataSourceFactory(this, userAgent))
            .createMediaSource(uri)

        binding.epVideoView.player = exoPlayer

        exoPlayer.prepare(mediaSource)
        exoPlayer.playWhenReady = true
    }

    override fun onStart() {
        super.onStart()
        playVideo()
    }

    private fun playVideo() {
        val file = File(uri)
        val localUri = Uri.fromFile(file)
        play(localUri)
    }

    override fun onStop() {
        super.onStop()
        exoPlayer.stop()
        exoPlayer.release()
    }
}


================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#008577"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>


================================================
FILE: app/src/main/res/drawable/ic_play_white_24dp.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
	android:width="50dp"
	android:height="50dp"
	android:viewportHeight="24.0"
	android:viewportWidth="24.0">

	<path
		android:fillColor="#c4ffffff"
		android: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" />

</vector>


================================================
FILE: app/src/main/res/drawable/ic_video_library_white_24dp.xml
================================================
<vector android:height="24dp" android:tint="#FFFFFF"
    android:viewportHeight="24.0" android:viewportWidth="24.0"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <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"/>
</vector>


================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillType="evenOdd"
        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"
        android:strokeWidth="1"
        android:strokeColor="#00000000">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="78.5885"
                android:endY="90.9159"
                android:startX="48.7653"
                android:startY="61.0927"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        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"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>


================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appBarLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <RelativeLayout
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:popupTheme="@style/AppTheme.PopupOverlay">

            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:text="@string/home_title"
                android:textColor="@color/colorWhite"
                android:textSize="16sp" />

            <Button
                android:id="@+id/cancel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:layout_centerVertical="true"
                android:text="cancel" />
        </RelativeLayout>

    </com.google.android.material.appbar.AppBarLayout>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/mainContents"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_margin="16dp"
        android:visibility="gone"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        app:layout_constraintBottom_toTopOf="@+id/linearLayout"
        app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
        tools:layout_editor_absoluteX="16dp">


        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />


    </androidx.constraintlayout.widget.ConstraintLayout>

    <LinearLayout
        android:id="@+id/linearLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:orientation="horizontal"
        android:padding="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/pickVideo"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_weight="1"
            android:background="@color/colorBrown"
            android:text="@string/pick_video"
            android:textColor="@color/colorWhite" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/recordVideo"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:background="@color/colorBrown"
            android:text="@string/record_video"

            android:textColor="@color/colorWhite" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

================================================
FILE: app/src/main/res/layout/activity_video_player.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorBlack">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/ep_video_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

================================================
FILE: app/src/main/res/layout/content_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:minWidth="120dp"
    android:visibility="visible"
    app:cardCornerRadius="5dp"
    app:cardElevation="0dp"
    app:cardMaxElevation="0dp"
    app:cardPreventCornerOverlap="true"
    app:cardUseCompatPadding="true">

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/videoImage"
            android:layout_width="wrap_content"
            android:layout_height="150dp"
            android:minWidth="150dp"
            android:scaleType="centerCrop"
            tools:background="@tools:sample/avatars" />

        <ImageView
            android:id="@+id/playPause"
            android:layout_width="42dp"
            android:layout_height="42dp"
            android:layout_gravity="center"
            android:background="@android:color/transparent"
            android:src="@drawable/ic_play_white_24dp" />


    </FrameLayout>

</com.google.android.material.card.MaterialCardView>

================================================
FILE: app/src/main/res/layout/recycler_view_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/videoLayout"
        layout="@layout/content_main"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="16dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <TextView
        android:id="@+id/progress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:gravity="center_horizontal"
        android:textColor="@color/colorPrimary"
        android:textSize="32sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/videoLayout"
        tools:text="Progress" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/progress"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:max="100"
        android:progress="0"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progress" />

    <TextView
        android:id="@+id/newSize"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:textColor="@color/colorBlack"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progressBar"
        tools:text="Size after compression" />

    <View
        android:id="@+id/divider"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginTop="16dp"
        android:background="?android:attr/listDivider"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/newSize" />

</androidx.constraintlayout.widget.ConstraintLayout>


================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#344772</color>
    <color name="colorPrimaryDark">#002046</color>
    <color name="colorAccent">#6272a1</color>
    <color name="colorBrown">#A52A2A</color>
    <color name="colorWhite">#FFFFFF</color>
    <color name="colorBlack">#000000</color>
    <color name="colorBackground">#F5F5F6</color>
</resources>


================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<resources>
    <dimen name="fab_margin">16dp</dimen>
</resources>


================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">VideoCompressor</string>
    <string name="home_title">Video Compressor Sample</string>
    <string name="pick_video">Pick Video</string>
    <string name="record_video">Record Video</string>
</resources>


================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowBackground">@color/colorBackground</item>
    </style>

    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />

</resources>


================================================
FILE: app/src/main/res/xml/media_capabilities.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<media-capabilities xmlns:android="http://schemas.android.com/apk/res/android">
    <format android:name="HEVC" supported="false"/>
    <format android:name="HDR10" supported="false"/>
    <format android:name="HDR10Plus" supported="false"/>
    <format android:name="Dolby-Vision" supported="false"/>
    <format android:name="HLG" supported="false"/>
    <format android:name="SlowMotion" supported="false"/>
</media-capabilities>

================================================
FILE: app/src/test/java/com/abedelazizshe/lightcompressor/ExampleUnitTest.kt
================================================
package com.abedelazizshe.lightcompressor

import org.junit.Test

import org.junit.Assert.*

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}


================================================
FILE: build.gradle
================================================
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.4.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21"
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Sat Jan 25 15:17:42 SGT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

================================================
FILE: gradle.properties
================================================
android.enableJetifier=true
android.injected.testOnly=false
android.lifecycleProcessor.incremental=true
android.useAndroidX=true
kapt.include.compile.classpath=false
kapt.incremental.apt=true
kapt.verbose=true
kotlin.code.style=official
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx1536m
org.gradle.parallel=true

================================================
FILE: gradlew
================================================
#!/usr/bin/env sh

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn () {
    echo "$*"
}

die () {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
  NONSTOP* )
    nonstop=true
    ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=$((i+1))
    done
    case $i in
        (0) set -- ;;
        (1) set -- "$args0" ;;
        (2) set -- "$args0" "$args1" ;;
        (3) set -- "$args0" "$args1" "$args2" ;;
        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Escape application args
save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
}
APP_ARGS=$(save "$@")

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
  cd "$(dirname "$0")"
fi

exec "$JAVACMD" "$@"


================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto init

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:init
@rem Get command-line arguments, handling Windows variants

if not "%OS%" == "Windows_NT" goto win9xME_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: jitpack.yml
================================================
jdk:
  - openjdk11

================================================
FILE: lightcompressor/.idea/.gitignore
================================================
# Default ignored files
/workspace.xml

================================================
FILE: lightcompressor/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'

android {
    compileSdkVersion 33
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 33

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
        }
    }

    publishing {
        singleVariant("release") {
            withSourcesJar()
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.21"
    implementation "androidx.core:core-ktx:1.10.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
    implementation "com.googlecode.mp4parser:isoparser:1.0.6"

    testImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test.ext:junit:1.1.5"
    androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
}

afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                from components.release
                groupId = "com.github.AbedElazizShe"
                artifactId = "LightCompressor"
                version = '1.3.3'
            }
        }
    }
}

================================================
FILE: lightcompressor/src/androidTest/java/com/abedelazizshe/lightcompressorlibrary/ExampleInstrumentedTest.kt
================================================
package com.abedelazizshe.lightcompressorlibrary

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.abedelazizshe.lightcompressorlibrary.test", appContext.packageName)
    }
}


================================================
FILE: lightcompressor/src/main/AndroidManifest.xml
================================================
<manifest package="com.abedelazizshe.lightcompressorlibrary" />


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt
================================================
package com.abedelazizshe.lightcompressorlibrary

import androidx.annotation.MainThread
import androidx.annotation.WorkerThread

/**
 * Created by AbedElaziz Shehadeh on 27 Jan, 2020
 * elaziz.shehadeh@gmail.com
 */
interface CompressionListener {
    @MainThread
    fun onStart(index: Int)

    @MainThread
    fun onSuccess(index: Int, size: Long, path: String?)

    @MainThread
    fun onFailure(index: Int, failureMessage: String)

    @WorkerThread
    fun onProgress(index: Int, percent: Float)

    @WorkerThread
    fun onCancelled(index: Int)
}

interface CompressionProgressListener {
    fun onProgressChanged(index: Int, percent: Float)
    fun onProgressCancelled(index: Int)
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt
================================================
package com.abedelazizshe.lightcompressorlibrary

import android.content.ContentValues
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import com.abedelazizshe.lightcompressorlibrary.compressor.Compressor.compressVideo
import com.abedelazizshe.lightcompressorlibrary.compressor.Compressor.isRunning
import com.abedelazizshe.lightcompressorlibrary.config.*
import com.abedelazizshe.lightcompressorlibrary.utils.saveVideoInExternal
import com.abedelazizshe.lightcompressorlibrary.video.Result
import kotlinx.coroutines.*
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException


enum class VideoQuality {
    VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOW
}

object VideoCompressor : CoroutineScope by MainScope() {

    private var job: Job? = null

    /**
     * This function compresses a given list of [uris] of video files and writes the compressed
     * video file at [SharedStorageConfiguration.saveAt] directory, or at [AppSpecificStorageConfiguration.subFolderName]
     *
     * The source videos should be provided content uris.
     *
     * Only [sharedStorageConfiguration] or [appSpecificStorageConfiguration] must be specified at a
     * time. Passing both will throw an Exception.
     *
     * @param [context] the application context.
     * @param [uris] the list of content Uris of the video files.
     * @param [isStreamable] determines if the output video should be prepared for streaming.
     * @param [sharedStorageConfiguration] configuration for the path directory where the compressed
     * videos will be saved, and the name of the file
     * @param [appSpecificStorageConfiguration] configuration for the path directory where the compressed
     * videos will be saved, the name of the file, and any sub-folders name. The library won't create the subfolder
     * and will throw an exception if the subfolder does not exist.
     * @param [listener] a compression listener that listens to compression [CompressionListener.onStart],
     * [CompressionListener.onProgress], [CompressionListener.onFailure], [CompressionListener.onSuccess]
     * and if the compression was [CompressionListener.onCancelled]
     * @param [configureWith] to allow add video compression configuration that could be:
     * [Configuration.quality] to allow choosing a video quality that can be [VideoQuality.LOW],
     * [VideoQuality.MEDIUM], [VideoQuality.HIGH], and [VideoQuality.VERY_HIGH].
     * This defaults to [VideoQuality.MEDIUM]
     * [Configuration.isMinBitrateCheckEnabled] to determine if the checking for a minimum bitrate threshold
     * before compression is enabled or not. This default to `true`
     * [Configuration.videoBitrateInMbps] which is a custom bitrate for the video. You might consider setting
     * [Configuration.isMinBitrateCheckEnabled] to `false` if your bitrate is less than 2000000.
     *  * [Configuration.keepOriginalResolution] to keep the original video height and width when compressing.
     * This defaults to `false`
     * [Configuration.videoHeight] which is a custom height for the video. Must be specified with [Configuration.videoWidth]
     * [Configuration.videoWidth] which is a custom width for the video. Must be specified with [Configuration.videoHeight]
     */
    @JvmStatic
    @JvmOverloads
    fun start(
        context: Context,
        uris: List<Uri>,
        isStreamable: Boolean = false,
        storageConfiguration: StorageConfiguration,
        configureWith: Configuration,
        listener: CompressionListener,
    ) {
        // Only one is allowed
        assert(configureWith.videoNames.size == uris.size)

        doVideoCompression(
            context,
            uris,
            isStreamable,
            storageConfiguration,
            configureWith,
            listener,
        )
    }

    /**
     * Call this function to cancel video compression process which will call [CompressionListener.onCancelled]
     */
    @JvmStatic
    fun cancel() {
        job?.cancel()
        isRunning = false
    }

    private fun doVideoCompression(
        context: Context,
        uris: List<Uri>,
        isStreamable: Boolean,
        storageConfiguration: StorageConfiguration,
        configuration: Configuration,
        listener: CompressionListener,
    ) {
        var streamableFile: File? = null
        for (i in uris.indices) {

            val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
                listener.onFailure(i, throwable.message ?: "")
            }
            val coroutineScope = CoroutineScope(Job() + coroutineExceptionHandler)

            job = coroutineScope.launch(Dispatchers.IO) {

                val job = async { getMediaPath(context, uris[i]) }
                val path = job.await()

                val desFile = saveVideoFile(
                    context,
                    path,
                    storageConfiguration,
                    isStreamable,
                    configuration.videoNames[i],
                    shouldSave = false
                )

                if (isStreamable)
                    streamableFile = saveVideoFile(
                        context,
                        path,
                        storageConfiguration,
                        null,
                        configuration.videoNames[i],
                        shouldSave = false
                    )

                desFile?.let {
                    isRunning = true
                    listener.onStart(i)
                    val result = startCompression(
                        i,
                        context,
                        uris[i],
                        desFile.path,
                        streamableFile?.path,
                        configuration,
                        listener,
                    )

                    // Runs in Main(UI) Thread
                    if (result.success) {
                        val savedFile = saveVideoFile(
                            context,
                            result.path,
                            storageConfiguration,
                            isStreamable,
                            configuration.videoNames[i],
                            shouldSave = true
                        )

                        listener.onSuccess(i, result.size, savedFile?.path)
                    } else {
                        listener.onFailure(i, result.failureMessage ?: "An error has occurred!")
                    }
                }
            }
        }
    }

    private suspend fun startCompression(
        index: Int,
        context: Context,
        srcUri: Uri,
        destPath: String,
        streamableFile: String? = null,
        configuration: Configuration,
        listener: CompressionListener,
    ): Result = withContext(Dispatchers.Default) {
        return@withContext compressVideo(
            index,
            context,
            srcUri,
            destPath,
            streamableFile,
            configuration,
            object : CompressionProgressListener {
                override fun onProgressChanged(index: Int, percent: Float) {
                    listener.onProgress(index, percent)
                }

                override fun onProgressCancelled(index: Int) {
                    listener.onCancelled(index)
                }
            },
        )
    }

    private fun saveVideoFile(
        context: Context,
        filePath: String?,
        storageConfiguration: StorageConfiguration,
        isStreamable: Boolean?,
        videoName: String,
        shouldSave: Boolean
    ): File? {
        return filePath?.let {
            val videoFile = File(filePath)
            storageConfiguration.createFileToSave(
                context,
                videoFile,
                validatedFileName(
                    videoName,
                    isStreamable
                ),
                shouldSave
            )
        }
    }

    private fun getMediaPath(context: Context, uri: Uri): String {

        val resolver = context.contentResolver
        val projection = arrayOf(MediaStore.Video.Media.DATA)
        var cursor: Cursor? = null
        try {
            cursor = resolver.query(uri, projection, null, null, null)
            return if (cursor != null) {
                val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)
                cursor.moveToFirst()
                cursor.getString(columnIndex)

            } else throw Exception()

        } catch (e: Exception) {
            resolver.let {
                val filePath = (context.applicationInfo.dataDir + File.separator
                        + System.currentTimeMillis())
                val file = File(filePath)

                resolver.openInputStream(uri)?.use { inputStream ->
                    FileOutputStream(file).use { outputStream ->
                        val buf = ByteArray(4096)
                        var len: Int
                        while (inputStream.read(buf).also { len = it } > 0) outputStream.write(
                            buf,
                            0,
                            len
                        )
                    }
                }
                return file.absolutePath
            }
        } finally {
            cursor?.close()
        }
    }

    private fun validatedFileName(name: String, isStreamable: Boolean?): String {
        val videoName = if (isStreamable == null || !isStreamable) name
        else "${name}_temp"

        if (!videoName.contains("mp4")) return "${videoName}.mp4"
        return videoName
    }
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.compressor

import android.content.Context
import android.media.*
import android.net.Uri
import android.os.Build
import android.util.Log
import com.abedelazizshe.lightcompressorlibrary.CompressionProgressListener
import com.abedelazizshe.lightcompressorlibrary.config.Configuration
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.findTrack
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.getBitrate
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.hasQTI
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.prepareVideoHeight
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.prepareVideoWidth
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.printException
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.setOutputFileParameters
import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils.setUpMP4Movie
import com.abedelazizshe.lightcompressorlibrary.utils.StreamableVideo
import com.abedelazizshe.lightcompressorlibrary.utils.roundDimension
import com.abedelazizshe.lightcompressorlibrary.video.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.ByteBuffer

/**
 * Created by AbedElaziz Shehadeh on 27 Jan, 2020
 * elaziz.shehadeh@gmail.com
 */
object Compressor {

    // 2Mbps
    private const val MIN_BITRATE = 2000000

    // H.264 Advanced Video Coding
    private const val MIME_TYPE = "video/avc"
    private const val MEDIACODEC_TIMEOUT_DEFAULT = 100L

    private const val INVALID_BITRATE =
        "The provided bitrate is smaller than what is needed for compression " +
                "try to set isMinBitRateEnabled to false"

    var isRunning = true

    suspend fun compressVideo(
        index: Int,
        context: Context,
        srcUri: Uri,
        destination: String,
        streamableFile: String?,
        configuration: Configuration,
        listener: CompressionProgressListener,
    ): Result = withContext(Dispatchers.Default) {

        val extractor = MediaExtractor()
        // Retrieve the source's metadata to be used as input to generate new values for compression
        val mediaMetadataRetriever = MediaMetadataRetriever()

        try {
            mediaMetadataRetriever.setDataSource(context, srcUri)
        } catch (exception: Exception) {
            printException(exception)
            return@withContext Result(
                index,
                success = false,
                failureMessage = "${exception.message}"
            )
        }

        runCatching {
            extractor.setDataSource(context, srcUri, null)
        }

        val height: Double = prepareVideoHeight(mediaMetadataRetriever)

        val width: Double = prepareVideoWidth(mediaMetadataRetriever)

        val rotationData =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)

        val bitrateData =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)

        val durationData =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)


        if (rotationData.isNullOrEmpty() || bitrateData.isNullOrEmpty() || durationData.isNullOrEmpty()) {
            // Exit execution
            return@withContext Result(
                index,
                success = false,
                failureMessage = "Failed to extract video meta-data, please try again"
            )
        }

        var (rotation, bitrate, duration) = try {
            Triple(rotationData.toInt(), bitrateData.toInt(), durationData.toLong() * 1000)
        } catch (e: java.lang.Exception) {
            return@withContext Result(
                index,
                success = false,
                failureMessage = "Failed to extract video meta-data, please try again"
            )
        }

        // Check for a min video bitrate before compression
        // Note: this is an experimental value
        if (configuration.isMinBitrateCheckEnabled && bitrate <= MIN_BITRATE)
            return@withContext Result(index, success = false, failureMessage = INVALID_BITRATE)

        //Handle new bitrate value
        val newBitrate: Int =
            if (configuration.videoBitrateInMbps == null) getBitrate(bitrate, configuration.quality)
            else configuration.videoBitrateInMbps!! * 1000000

        //Handle new width and height values
        val resizer = configuration.resizer
        val target = resizer?.resize(width, height) ?: Pair(width, height)
        var newWidth = roundDimension(target.first)
        var newHeight = roundDimension(target.second)

        //Handle rotation values and swapping height and width if needed
        rotation = when (rotation) {
            90, 270 -> {
                val tempHeight = newHeight
                newHeight = newWidth
                newWidth = tempHeight
                0
            }

            180 -> 0
            else -> rotation
        }

        return@withContext start(
            index,
            newWidth,
            newHeight,
            destination,
            newBitrate,
            streamableFile,
            configuration.disableAudio,
            extractor,
            listener,
            duration,
            rotation
        )
    }

    @Suppress("DEPRECATION")
    private fun start(
        id: Int,
        newWidth: Int,
        newHeight: Int,
        destination: String,
        newBitrate: Int,
        streamableFile: String?,
        disableAudio: Boolean,
        extractor: MediaExtractor,
        compressionProgressListener: CompressionProgressListener,
        duration: Long,
        rotation: Int
    ): Result {

        if (newWidth != 0 && newHeight != 0) {

            val cacheFile = File(destination)

            try {
                // MediaCodec accesses encoder and decoder components and processes the new video
                //input to generate a compressed/smaller size video
                val bufferInfo = MediaCodec.BufferInfo()

                // Setup mp4 movie
                val movie = setUpMP4Movie(rotation, cacheFile)

                // MediaMuxer outputs MP4 in this app
                val mediaMuxer = MP4Builder().createMovie(movie)

                // Start with video track
                val videoIndex = findTrack(extractor, isVideo = true)

                extractor.selectTrack(videoIndex)
                extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
                val inputFormat = extractor.getTrackFormat(videoIndex)

                val outputFormat: MediaFormat =
                    MediaFormat.createVideoFormat(MIME_TYPE, newWidth, newHeight)
                //set output format
                setOutputFileParameters(
                    inputFormat,
                    outputFormat,
                    newBitrate,
                )

                val decoder: MediaCodec

                val hasQTI = hasQTI()

                val encoder = prepareEncoder(outputFormat, hasQTI)

                val inputSurface: InputSurface
                val outputSurface: OutputSurface

                try {
                    var inputDone = false
                    var outputDone = false

                    var videoTrackIndex = -5

                    inputSurface = InputSurface(encoder.createInputSurface())
                    inputSurface.makeCurrent()
                    //Move to executing state
                    encoder.start()

                    outputSurface = OutputSurface()

                    decoder = prepareDecoder(inputFormat, outputSurface)

                    //Move to executing state
                    decoder.start()

                    while (!outputDone) {
                        if (!inputDone) {

                            val index = extractor.sampleTrackIndex

                            if (index == videoIndex) {
                                val inputBufferIndex =
                                    decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)
                                if (inputBufferIndex >= 0) {
                                    val inputBuffer = decoder.getInputBuffer(inputBufferIndex)
                                    val chunkSize = extractor.readSampleData(inputBuffer!!, 0)
                                    when {
                                        chunkSize < 0 -> {

                                            decoder.queueInputBuffer(
                                                inputBufferIndex,
                                                0,
                                                0,
                                                0L,
                                                MediaCodec.BUFFER_FLAG_END_OF_STREAM
                                            )
                                            inputDone = true
                                        }

                                        else -> {

                                            decoder.queueInputBuffer(
                                                inputBufferIndex,
                                                0,
                                                chunkSize,
                                                extractor.sampleTime,
                                                0
                                            )
                                            extractor.advance()

                                        }
                                    }
                                }

                            } else if (index == -1) { //end of file
                                val inputBufferIndex =
                                    decoder.dequeueInputBuffer(MEDIACODEC_TIMEOUT_DEFAULT)
                                if (inputBufferIndex >= 0) {
                                    decoder.queueInputBuffer(
                                        inputBufferIndex,
                                        0,
                                        0,
                                        0L,
                                        MediaCodec.BUFFER_FLAG_END_OF_STREAM
                                    )
                                    inputDone = true
                                }
                            }
                        }

                        var decoderOutputAvailable = true
                        var encoderOutputAvailable = true

                        loop@ while (decoderOutputAvailable || encoderOutputAvailable) {

                            if (!isRunning) {
                                dispose(
                                    videoIndex,
                                    decoder,
                                    encoder,
                                    inputSurface,
                                    outputSurface,
                                    extractor
                                )

                                compressionProgressListener.onProgressCancelled(id)
                                return Result(
                                    id,
                                    success = false,
                                    failureMessage = "The compression has stopped!"
                                )
                            }

                            //Encoder
                            val encoderStatus =
                                encoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)

                            when {
                                encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> encoderOutputAvailable =
                                    false

                                encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                                    val newFormat = encoder.outputFormat
                                    if (videoTrackIndex == -5)
                                        videoTrackIndex = mediaMuxer.addTrack(newFormat, false)
                                }

                                encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
                                    // ignore this status
                                }

                                encoderStatus < 0 -> throw RuntimeException("unexpected result from encoder.dequeueOutputBuffer: $encoderStatus")
                                else -> {
                                    val encodedData = encoder.getOutputBuffer(encoderStatus)
                                        ?: throw RuntimeException("encoderOutputBuffer $encoderStatus was null")

                                    if (bufferInfo.size > 1) {
                                        if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
                                            mediaMuxer.writeSampleData(
                                                videoTrackIndex,
                                                encodedData, bufferInfo, false
                                            )
                                        }

                                    }

                                    outputDone =
                                        bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0
                                    encoder.releaseOutputBuffer(encoderStatus, false)
                                }
                            }
                            if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) continue@loop

                            //Decoder
                            val decoderStatus =
                                decoder.dequeueOutputBuffer(bufferInfo, MEDIACODEC_TIMEOUT_DEFAULT)
                            when {
                                decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER -> decoderOutputAvailable =
                                    false

                                decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
                                    // ignore this status
                                }

                                decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                                    // ignore this status
                                }

                                decoderStatus < 0 -> throw RuntimeException("unexpected result from decoder.dequeueOutputBuffer: $decoderStatus")
                                else -> {
                                    val doRender = bufferInfo.size != 0

                                    decoder.releaseOutputBuffer(decoderStatus, doRender)
                                    if (doRender) {
                                        var errorWait = false
                                        try {
                                            outputSurface.awaitNewImage()
                                        } catch (e: Exception) {
                                            errorWait = true
                                            Log.e(
                                                "Compressor",
                                                e.message ?: "Compression failed at swapping buffer"
                                            )
                                        }

                                        if (!errorWait) {
                                            outputSurface.drawImage()

                                            inputSurface.setPresentationTime(bufferInfo.presentationTimeUs * 1000)

                                            compressionProgressListener.onProgressChanged(
                                                id,
                                                bufferInfo.presentationTimeUs.toFloat() / duration.toFloat() * 100
                                            )

                                            inputSurface.swapBuffers()
                                        }
                                    }
                                    if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                                        decoderOutputAvailable = false
                                        encoder.signalEndOfInputStream()
                                    }
                                }
                            }
                        }
                    }

                } catch (exception: Exception) {
                    printException(exception)
                    return Result(id, success = false, failureMessage = exception.message)
                }

                dispose(
                    videoIndex,
                    decoder,
                    encoder,
                    inputSurface,
                    outputSurface,
                    extractor
                )

                processAudio(
                    mediaMuxer = mediaMuxer,
                    bufferInfo = bufferInfo,
                    disableAudio = disableAudio,
                    extractor
                )

                extractor.release()
                try {
                    mediaMuxer.finishMovie()
                } catch (e: Exception) {
                    printException(e)
                }

            } catch (exception: Exception) {
                printException(exception)
            }

            var resultFile = cacheFile

            streamableFile?.let {
                try {
                    val result = StreamableVideo.start(`in` = cacheFile, out = File(it))
                    resultFile = File(it)
                    if (result && cacheFile.exists()) {
                        cacheFile.delete()
                    }

                } catch (e: Exception) {
                    printException(e)
                }
            }
            return Result(
                id,
                success = true,
                failureMessage = null,
                size = resultFile.length(),
                resultFile.path
            )
        }

        return Result(
            id,
            success = false,
            failureMessage = "Something went wrong, please try again"
        )
    }

    private fun processAudio(
        mediaMuxer: MP4Builder,
        bufferInfo: MediaCodec.BufferInfo,
        disableAudio: Boolean,
        extractor: MediaExtractor
    ) {
        val audioIndex = findTrack(extractor, isVideo = false)
        if (audioIndex >= 0 && !disableAudio) {
            extractor.selectTrack(audioIndex)
            val audioFormat = extractor.getTrackFormat(audioIndex)
            val muxerTrackIndex = mediaMuxer.addTrack(audioFormat, true)
            var maxBufferSize = audioFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)

            if (maxBufferSize <= 0) {
                maxBufferSize = 64 * 1024
            }

            var buffer: ByteBuffer = ByteBuffer.allocateDirect(maxBufferSize)
            if (Build.VERSION.SDK_INT >= 28) {
                val size = extractor.sampleSize
                if (size > maxBufferSize) {
                    maxBufferSize = (size + 1024).toInt()
                    buffer = ByteBuffer.allocateDirect(maxBufferSize)
                }
            }
            var inputDone = false
            extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)

            while (!inputDone) {
                val index = extractor.sampleTrackIndex
                if (index == audioIndex) {
                    bufferInfo.size = extractor.readSampleData(buffer, 0)

                    if (bufferInfo.size >= 0) {
                        bufferInfo.apply {
                            presentationTimeUs = extractor.sampleTime
                            offset = 0
                            flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
                        }
                        mediaMuxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo, true)
                        extractor.advance()

                    } else {
                        bufferInfo.size = 0
                        inputDone = true
                    }
                } else if (index == -1) {
                    inputDone = true
                }
            }
            extractor.unselectTrack(audioIndex)
        }
    }

    private fun prepareEncoder(outputFormat: MediaFormat, hasQTI: Boolean): MediaCodec {

        // This seems to cause an issue with certain phones
        // val encoderName = MediaCodecList(REGULAR_CODECS).findEncoderForFormat(outputFormat)
        // val encoder: MediaCodec = MediaCodec.createByCodecName(encoderName)
        // Log.i("encoderName", encoder.name)
        // c2.qti.avc.encoder results in a corrupted .mp4 video that does not play in
        // Mac and iphones
        var encoder = if (hasQTI) {
            MediaCodec.createByCodecName("c2.android.avc.encoder")
        } else {
            MediaCodec.createEncoderByType(MIME_TYPE)
        }

        try {
            encoder.configure(
                outputFormat, null, null,
                MediaCodec.CONFIGURE_FLAG_ENCODE
            )
        } catch (e: Exception) {
            encoder = MediaCodec.createEncoderByType(MIME_TYPE)
            encoder.configure(
                outputFormat, null, null,
                MediaCodec.CONFIGURE_FLAG_ENCODE
            )
        }

        return encoder
    }

    private fun prepareDecoder(
        inputFormat: MediaFormat,
        outputSurface: OutputSurface,
    ): MediaCodec {
        // This seems to cause an issue with certain phones
        // val decoderName =
        //    MediaCodecList(REGULAR_CODECS).findDecoderForFormat(inputFormat)
        // val decoder = MediaCodec.createByCodecName(decoderName)
        // Log.i("decoderName", decoder.name)

        // val decoder = if (hasQTI) {
        // MediaCodec.createByCodecName("c2.android.avc.decoder")
        //} else {

        val decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME)!!)
        //}

        decoder.configure(inputFormat, outputSurface.getSurface(), null, 0)

        return decoder
    }

    private fun dispose(
        videoIndex: Int,
        decoder: MediaCodec,
        encoder: MediaCodec,
        inputSurface: InputSurface,
        outputSurface: OutputSurface,
        extractor: MediaExtractor
    ) {
        extractor.unselectTrack(videoIndex)

        decoder.stop()
        decoder.release()

        encoder.stop()
        encoder.release()

        inputSurface.release()
        outputSurface.release()
    }
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.config

import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import com.abedelazizshe.lightcompressorlibrary.VideoQuality
import com.abedelazizshe.lightcompressorlibrary.utils.saveVideoInExternal
import java.io.File
import java.io.FileInputStream
import java.io.IOException

data class Configuration(
    var quality: VideoQuality = VideoQuality.MEDIUM,
    var isMinBitrateCheckEnabled: Boolean = true,
    var videoBitrateInMbps: Int? = null,
    var disableAudio: Boolean = false,
    val resizer: VideoResizer? = VideoResizer.auto,
    var videoNames: List<String>
) {
    @Deprecated("Use VideoResizer to override the output video dimensions.", ReplaceWith("Configuration(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, resizer = if (keepOriginalResolution) null else VideoResizer.auto, videoNames)"))
    constructor(
        quality: VideoQuality = VideoQuality.MEDIUM,
        isMinBitrateCheckEnabled: Boolean = true,
        videoBitrateInMbps: Int? = null,
        disableAudio: Boolean = false,
        keepOriginalResolution: Boolean,
        videoNames: List<String>) : this(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, getVideoResizer(keepOriginalResolution, null, null), videoNames)

    @Deprecated("Use VideoResizer to override the output video dimensions.", ReplaceWith("Configuration(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, resizer = VideoResizer.matchSize(videoWidth, videoHeight), videoNames)"))
    constructor(
        quality: VideoQuality = VideoQuality.MEDIUM,
        isMinBitrateCheckEnabled: Boolean = true,
        videoBitrateInMbps: Int? = null,
        disableAudio: Boolean = false,
        keepOriginalResolution: Boolean = false,
        videoHeight: Double? = null,
        videoWidth: Double? = null,
        videoNames: List<String>) : this(quality, isMinBitrateCheckEnabled, videoBitrateInMbps, disableAudio, getVideoResizer(keepOriginalResolution, videoHeight, videoWidth), videoNames)
}

private fun getVideoResizer(keepOriginalResolution: Boolean, videoHeight: Double?, videoWidth: Double?): VideoResizer? =
    if (keepOriginalResolution) {
        null
    } else if (videoWidth != null && videoHeight != null) {
        VideoResizer.matchSize(videoWidth, videoHeight, true)
    } else {
        VideoResizer.auto
    }

interface StorageConfiguration {
    fun createFileToSave(
        context: Context,
        videoFile: File,
        fileName: String,
        shouldSave: Boolean
    ): File
}

class AppSpecificStorageConfiguration(
    private val subFolderName: String? = null,
) : StorageConfiguration {

    override fun createFileToSave(
        context: Context,
        videoFile: File,
        fileName: String,
        shouldSave: Boolean
    ): File {
        val fullPath =
            if (subFolderName != null) "${subFolderName}/$fileName"
            else fileName

        if (!File("${context.filesDir}/$fullPath").exists()) {
            File("${context.filesDir}/$fullPath").parentFile?.mkdirs()
        }
        return File(context.filesDir, fullPath)
    }
}


enum class SaveLocation {
    pictures,
    downloads,
    movies,
}

class SharedStorageConfiguration(
    private val saveAt: SaveLocation? = null,
    private val subFolderName: String? = null,
) : StorageConfiguration {

    override fun createFileToSave(
        context: Context,
        videoFile: File,
        fileName: String,
        shouldSave: Boolean
    ): File {
        val saveLocation =
            when (saveAt) {
                SaveLocation.downloads -> {
                    Environment.DIRECTORY_DOWNLOADS
                }

                SaveLocation.pictures -> {
                    Environment.DIRECTORY_PICTURES
                }

                else -> {
                    Environment.DIRECTORY_MOVIES
                }
            }

        if (Build.VERSION.SDK_INT >= 29) {
            val fullPath =
                if (subFolderName != null) "$saveLocation/${subFolderName}"
                else saveLocation
            if (shouldSave) {
                saveVideoInExternal(context, fileName, fullPath, videoFile)
                File(context.filesDir, fileName).delete()
                return File("/storage/emulated/0/${fullPath}", fileName)
            }
            return File(context.filesDir, fileName)
        } else {
            val savePath =
                Environment.getExternalStoragePublicDirectory(saveLocation)

            val fullPath =
                if (subFolderName != null) "$savePath/${subFolderName}"
                else savePath.path

            val desFile = File(fullPath, fileName)

            if (!desFile.exists()) {
                try {
                    desFile.parentFile?.mkdirs()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }

            if (shouldSave) {
                context.openFileOutput(fileName, Context.MODE_PRIVATE)
                    .use { outputStream ->
                        FileInputStream(videoFile).use { inputStream ->
                            val buf = ByteArray(4096)
                            while (true) {
                                val sz = inputStream.read(buf)
                                if (sz <= 0) break
                                outputStream.write(buf, 0, sz)
                            }

                        }
                    }

            }
            return desFile
        }
    }
}

class CacheStorageConfiguration(
) : StorageConfiguration {
    override fun createFileToSave(
        context: Context,
        videoFile: File,
        fileName: String,
        shouldSave: Boolean
    ): File =
        File.createTempFile(videoFile.nameWithoutExtension,videoFile.extension)
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/VideoResizer.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.config

import com.abedelazizshe.lightcompressorlibrary.utils.CompressorUtils

fun interface VideoResizer {
    companion object {
        /**
         * Shrinks the video's resolution based on its original width and height.
         * - 50% If the width or height is greater than or equal to 1920 pixels.
         * - 75% If the width or height is greater than or equal to 1280 pixels.
         * - 95% If the width or height is greater than or equal to 960 pixels.
         * - 90% If the width and height are both less than 960 pixels.
         */
        @JvmStatic
        val auto: VideoResizer = ScaleResize(null);

        /**
         * Resize the video dimensions by the given scale factor
         */
        @JvmStatic
        fun scale(value: Double): VideoResizer = ScaleResize(value)

        /**
         * Scale the video down if the width or height are greater than [limit], retaining the video's aspect ratio.
         * @param limit The maximum width and height of the video
         */
        @JvmStatic
        fun limitSize(limit: Double): VideoResizer = LimitDimension(limit, limit)

        /**
         * Scale the video down if the width or height are greater than [maxWidth] or [maxHeight], retaining the video's aspect ratio.
         * @param maxWidth The maximum width of the video
         * @param maxHeight The maximum height of the video
         */
        @JvmStatic
        fun limitSize(maxWidth: Double, maxHeight: Double): VideoResizer = LimitDimension(maxWidth, maxHeight)

        /**
         * Scales the video so that the width and height matches [size], retaining the video's aspect ratio.
         * @param size The target width/height of the video
         */
        @JvmStatic
        fun matchSize(size: Double, stretch: Boolean = false): VideoResizer = MatchDimension(size, size, stretch)

        /**
         * Scales the video so that the width matches [width] or the height matches [height], retaining the video's aspect ratio.
         * @param width The target width of the video
         * @param height The target height of the video
         */
        @JvmStatic
        fun matchSize(width: Double, height: Double, stretch: Boolean = false): VideoResizer = MatchDimension(width, height, stretch)

        private fun keepAspect(width: Double, height: Double, newWidth: Double, newHeight: Double): Pair<Double, Double> {
            val desiredAspect = width / height
            val videoAspect = newWidth / newHeight
            return if (videoAspect <= desiredAspect) Pair(newWidth, newWidth / desiredAspect) else Pair(newHeight * desiredAspect, newHeight)
        }
    }

    fun resize(width: Double, height: Double): Pair<Double, Double>

    private class LimitDimension(private val width: Double, private val height: Double) : VideoResizer {
        override fun resize(width: Double, height: Double): Pair<Double, Double> {
            return if (width < this.width && height < this.height) Pair(width, height) else keepAspect(width, height, this.width, this.height)
        }
    }

    private class MatchDimension(private val width: Double, private val height: Double, private val stretch: Boolean) : VideoResizer {
        override fun resize(width: Double, height: Double): Pair<Double, Double> {
            return if (stretch) Pair(this.width, this.height) else keepAspect(width, height, this.width, this.height)
        }
    }

    private class ScaleResize(private val percentage: Double? = null) : VideoResizer {
        override fun resize(width: Double, height: Double): Pair<Double, Double> {
            val p = percentage ?: CompressorUtils.autoResizePercentage(width, height)
            return Pair(width * p, height * p)
        }
    }
}

================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/data/Atoms.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.data

import java.nio.ByteBuffer
import java.nio.ByteOrder

/*
FOURCC is short for "four character code" - an identifier for a video codec, compression format,
color or pixel format used in media files.

A character in this context is a 1 byte/8 bit value, thus a FOURCC always takes up exactly 32 bits/4
bytes in a file.
*/
fun fourCcToInt(byteArray: ByteArray): Int {
    // The bytes of a byteArray value are ordered from most significant to least significant.
    return ByteBuffer.wrap(byteArray).order(ByteOrder.BIG_ENDIAN).int
}

// Unused space available in file.
val FREE_ATOM =
    fourCcToInt(
        byteArrayOf(
            'f'.code.toByte(),
            'r'.code.toByte(),
            'e'.code.toByte(),
            'e'.code.toByte()
        )
    )

val JUNK_ATOM =
    fourCcToInt(
        byteArrayOf(
            'j'.code.toByte(),
            'u'.code.toByte(),
            'n'.code.toByte(),
            'k'.code.toByte()
        )
    )

// Movie sample data— media samples such as video frames and groups of audio samples. Usually this
// data can be interpreted only by using the movie resource.
val MDAT_ATOM =
    fourCcToInt(
        byteArrayOf(
            'm'.code.toByte(),
            'd'.code.toByte(),
            'a'.code.toByte(),
            't'.code.toByte()
        )
    )

// Movie resource metadata about the movie (number and type of tracks, location of sample data,
// and so on). Describes where the movie data can be found and how to interpret it.
val MOOV_ATOM =
    fourCcToInt(
        byteArrayOf(
            'm'.code.toByte(),
            'o'.code.toByte(),
            'o'.code.toByte(),
            'v'.code.toByte()
        )
    )

// Reference to movie preview data.
val PNOT_ATOM =
    fourCcToInt(
        byteArrayOf(
            'p'.code.toByte(),
            'n'.code.toByte(),
            'o'.code.toByte(),
            't'.code.toByte()
        )
    )

// Unused space available in file.
val SKIP_ATOM =
    fourCcToInt(
        byteArrayOf(
            's'.code.toByte(),
            'k'.code.toByte(),
            'i'.code.toByte(),
            'p'.code.toByte()
        )
    )

// Reserved space—can be overwritten by an extended size field if the following atom exceeds 2^32
// bytes, without displacing the contents of the following atom.
val WIDE_ATOM =
    fourCcToInt(
        byteArrayOf(
            'w'.code.toByte(),
            'i'.code.toByte(),
            'd'.code.toByte(),
            'e'.code.toByte()
        )
    )

val PICT_ATOM =
    fourCcToInt(
        byteArrayOf(
            'P'.code.toByte(),
            'I'.code.toByte(),
            'C'.code.toByte(),
            'T'.code.toByte()
        )
    )

// File type compatibility— identifies the file type and differentiates it from similar file
// types, such as MPEG-4 files and JPEG-2000 files.
val FTYP_ATOM =
    fourCcToInt(
        byteArrayOf(
            'f'.code.toByte(),
            't'.code.toByte(),
            'y'.code.toByte(),
            'p'.code.toByte()
        )
    )

val UUID_ATOM =
    fourCcToInt(
        byteArrayOf(
            'u'.code.toByte(),
            'u'.code.toByte(),
            'i'.code.toByte(),
            'd'.code.toByte()
        )
    )

val CMOV_ATOM =
    fourCcToInt(
        byteArrayOf(
            'c'.code.toByte(),
            'm'.code.toByte(),
            'o'.code.toByte(),
            'v'.code.toByte()
        )
    )

val STCO_ATOM =
    fourCcToInt(
        byteArrayOf(
            's'.code.toByte(),
            't'.code.toByte(),
            'c'.code.toByte(),
            'o'.code.toByte()
        )
    )

val CO64_ATOM =
    fourCcToInt(
        byteArrayOf(
            'c'.code.toByte(),
            'o'.code.toByte(),
            '6'.code.toByte(),
            '4'.code.toByte()
        )
    )


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/CompressorUtils.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.utils

import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.media.MediaExtractor
import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.os.Build
import android.util.Log
import com.abedelazizshe.lightcompressorlibrary.VideoQuality
import com.abedelazizshe.lightcompressorlibrary.video.Mp4Movie
import java.io.File
import kotlin.math.roundToInt

object CompressorUtils {

    private const val MIN_HEIGHT = 640.0
    private const val MIN_WIDTH = 368.0

    // 1 second between I-frames
    private const val I_FRAME_INTERVAL = 1

    fun prepareVideoWidth(
        mediaMetadataRetriever: MediaMetadataRetriever,
    ): Double {
        val widthData =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
        return if (widthData.isNullOrEmpty()) {
            MIN_WIDTH
        } else {
            widthData.toDouble()
        }
    }

    fun prepareVideoHeight(
        mediaMetadataRetriever: MediaMetadataRetriever,
    ): Double {
        val heightData =
            mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
        return if (heightData.isNullOrEmpty()) {
            MIN_HEIGHT
        } else {
            heightData.toDouble()
        }
    }

    /**
     * Setup movie with the height, width, and rotation values
     * @param rotation video rotation
     *
     * @return set movie with new values
     */
    fun setUpMP4Movie(
        rotation: Int,
        cacheFile: File,
    ): Mp4Movie {
        val movie = Mp4Movie()
        movie.apply {
            setCacheFile(cacheFile)
            setRotation(rotation)
        }

        return movie
    }

    /**
     * Set output parameters like bitrate and frame rate
     */
    fun setOutputFileParameters(
        inputFormat: MediaFormat,
        outputFormat: MediaFormat,
        newBitrate: Int,
    ) {
        val newFrameRate = getFrameRate(inputFormat)
        val iFrameInterval = getIFrameIntervalRate(inputFormat)
        outputFormat.apply {

            // according to https://developer.android.com/media/optimize/sharing#b-frames_and_encoding_profiles
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val type = outputFormat.getString(MediaFormat.KEY_MIME)
                val higherLevel = getHighestCodecProfileLevel(type)
                Log.i("Output file parameters", "Selected CodecProfileLevel: $higherLevel")
                setInteger(MediaFormat.KEY_PROFILE, higherLevel)
            } else {
                setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)
            }

            setInteger(
                MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
            )

            setInteger(MediaFormat.KEY_FRAME_RATE, newFrameRate)
            setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
            // expected bps
            setInteger(MediaFormat.KEY_BIT_RATE, newBitrate)
            setInteger(
                MediaFormat.KEY_BITRATE_MODE,
                MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
            )



            getColorStandard(inputFormat)?.let {
                setInteger(MediaFormat.KEY_COLOR_STANDARD, it)
            }

            getColorTransfer(inputFormat)?.let {
                setInteger(MediaFormat.KEY_COLOR_TRANSFER, it)
            }

            getColorRange(inputFormat)?.let {
                setInteger(MediaFormat.KEY_COLOR_RANGE, it)
            }


            Log.i(
                "Output file parameters",
                "videoFormat: $this"
            )
        }
    }

    private fun getFrameRate(format: MediaFormat): Int {
        return if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) format.getInteger(MediaFormat.KEY_FRAME_RATE)
        else 30
    }

    private fun getIFrameIntervalRate(format: MediaFormat): Int {
        return if (format.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) format.getInteger(
            MediaFormat.KEY_I_FRAME_INTERVAL
        )
        else I_FRAME_INTERVAL
    }

    private fun getColorStandard(format: MediaFormat): Int? {
        return if (format.containsKey(MediaFormat.KEY_COLOR_STANDARD)) format.getInteger(
            MediaFormat.KEY_COLOR_STANDARD
        )
        else null
    }

    private fun getColorTransfer(format: MediaFormat): Int? {
        return if (format.containsKey(MediaFormat.KEY_COLOR_TRANSFER)) format.getInteger(
            MediaFormat.KEY_COLOR_TRANSFER
        )
        else null
    }

    private fun getColorRange(format: MediaFormat): Int? {
        return if (format.containsKey(MediaFormat.KEY_COLOR_RANGE)) format.getInteger(
            MediaFormat.KEY_COLOR_RANGE
        )
        else null
    }

    /**
     * Counts the number of tracks (video, audio) found in the file source provided
     * @param extractor what is used to extract the encoded data
     * @param isVideo to determine whether we are processing video or audio at time of call
     * @return index of the requested track
     */
    fun findTrack(
        extractor: MediaExtractor,
        isVideo: Boolean,
    ): Int {
        val numTracks = extractor.trackCount
        for (i in 0 until numTracks) {
            val format = extractor.getTrackFormat(i)
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (isVideo) {
                if (mime?.startsWith("video/")!!) return i
            } else {
                if (mime?.startsWith("audio/")!!) return i
            }
        }
        return -5
    }

    fun printException(exception: Exception) {
        var message = "An error has occurred!"
        exception.localizedMessage?.let {
            message = it
        }
        Log.e("Compressor", message, exception)
    }

    /**
     * Get fixed bitrate value based on the file's current bitrate
     * @param bitrate file's current bitrate
     * @return new smaller bitrate value
     */
    fun getBitrate(
        bitrate: Int,
        quality: VideoQuality,
    ): Int {
        return when (quality) {
            VideoQuality.VERY_LOW -> (bitrate * 0.1).roundToInt()
            VideoQuality.LOW -> (bitrate * 0.2).roundToInt()
            VideoQuality.MEDIUM -> (bitrate * 0.3).roundToInt()
            VideoQuality.HIGH -> (bitrate * 0.4).roundToInt()
            VideoQuality.VERY_HIGH -> (bitrate * 0.6).roundToInt()
        }
    }

    /**
     * Generate new width and height for source file
     * @param width file's original width
     * @param height file's original height
     * @return the scale factor to apply to the video's resolution
     */
    fun autoResizePercentage(width: Double, height: Double): Double {
        return when {
            width >= 1920 || height >= 1920 -> 0.5
            width >= 1280 || height >= 1280 -> 0.75
            width >= 960 || height >= 960 -> 0.95
            else -> 0.9
        }
    }

    fun hasQTI(): Boolean {
        val list = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos
        for (codec in list) {
            Log.i("CODECS: ", codec.name)
            if (codec.name.contains("qti.avc")) {
                return true
            }
        }
        return false
    }

    /**
     * Get the highest profile level supported by the AVC encoder: High > Main > Baseline
     */
    private fun getHighestCodecProfileLevel(type: String?): Int {
        if (type == null) {
            return MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline
        }
        val list = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos
        val capabilities = list
            .filter { codec -> type in codec.supportedTypes && codec.name.contains("encoder") }
            .mapNotNull { codec -> codec.getCapabilitiesForType(type) }

        capabilities.forEach { capabilitiesForType ->
            val levels =  capabilitiesForType.profileLevels.map { it.profile }
            return when {
                MediaCodecInfo.CodecProfileLevel.AVCProfileHigh in levels -> MediaCodecInfo.CodecProfileLevel.AVCProfileHigh
                MediaCodecInfo.CodecProfileLevel.AVCProfileMain in levels -> MediaCodecInfo.CodecProfileLevel.AVCProfileMain
                else -> MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline
            }
        }

        return MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline
    }
}

/*
            if (codec.name == "c2.qti.avc.encoder") {
                val capabilities = codec.getCapabilitiesForType("video/avc")


                for (c in capabilities.colorFormats) {
                    Log.wtf("color format", c.toString())
                }

                for (c in capabilities.profileLevels) {
                    Log.wtf(" level", c.level.toString())
                    Log.wtf("profile ", c.profile.toString())
                }

                Log.wtf(
                    "complexity range",
                    capabilities.encoderCapabilities.complexityRange.upper.toString()
                )

                Log.wtf(
                    "quality range", " ${ capabilities.encoderCapabilities.qualityRange}"
                )

                Log.wtf(
                    "frame rates range", " ${ capabilities.videoCapabilities.supportedFrameRates}"
                )

                Log.wtf(
                    "bitrate rates range", " ${ capabilities.videoCapabilities.bitrateRange}"
                )

                Log.wtf(
                    "mode supported", " ${ capabilities.encoderCapabilities.isBitrateModeSupported(1)}"
                )

                Log.wtf(
                    "height alignment", " ${ capabilities.videoCapabilities.heightAlignment}"
                )

                Log.wtf(
                    "supported heights", " ${ capabilities.videoCapabilities.supportedHeights}"
                )

                Log.wtf(
                    "supported points", " ${ capabilities.videoCapabilities.supportedPerformancePoints}"
                )

                Log.wtf(
                    "supported width", " ${ capabilities.videoCapabilities.supportedWidths}"
                )

                Log.wtf(
                    "width alignment", " ${ capabilities.videoCapabilities.widthAlignment}"
                )

                Log.wtf(
                    "default format", " ${ capabilities.defaultFormat}"
                )

                Log.wtf(
                    "mime", " ${ capabilities.mimeType}"
                )

            }
 */

================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/FileUtils.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.utils

import android.content.ContentValues
import android.content.Context
import android.os.Environment
import android.provider.MediaStore
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream

fun saveVideoInExternal(
    context: Context,
    videoFileName: String,
    saveLocation: String,
    videoFile: File
) {
    val values = ContentValues().apply {

        put(
            MediaStore.Images.Media.DISPLAY_NAME,
            videoFileName
        )
        put(MediaStore.Images.Media.MIME_TYPE, "video/mp4")
        put(MediaStore.Images.Media.RELATIVE_PATH, saveLocation)
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    var collection =
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

    if (saveLocation == Environment.DIRECTORY_DOWNLOADS) {
        collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI
    }

    val fileUri = context.contentResolver.insert(collection, values)

    fileUri?.let {
        context.contentResolver.openFileDescriptor(fileUri, "rw")
            .use { descriptor ->
                descriptor?.let {
                    FileOutputStream(descriptor.fileDescriptor).use { out ->
                        FileInputStream(videoFile).use { inputStream ->
                            val buf = ByteArray(4096)
                            while (true) {
                                val sz = inputStream.read(buf)
                                if (sz <= 0) break
                                out.write(buf, 0, sz)
                            }
                        }
                    }
                }
            }

        values.clear()
        values.put(MediaStore.Video.Media.IS_PENDING, 0)
        context.contentResolver.update(fileUri, values, null, null)
    }
}



================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/NumbersUtils.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.utils

import kotlin.math.roundToInt

fun uInt32ToLong(int32: Int): Long {
    return int32.toLong()
}

fun uInt32ToInt(uInt32: Long): Int {
    if (uInt32 > Int.MAX_VALUE || uInt32 < 0) {
        throw Exception("uInt32 value is too large")
    }
    return uInt32.toInt()
}

fun uInt64ToLong(uInt64: Long): Long {
    if (uInt64 < 0) throw Exception("uInt64 value is too large")
    return uInt64
}


fun uInt32ToInt(uInt32: Int): Int {
    if (uInt32 < 0) {
        throw Exception("uInt32 value is too large")
    }
    return uInt32
}

private fun roundEven(value: Int): Int = value + 1 and 1.inv()

fun roundDimension(value: Double): Int =
    roundEven(((value / 16).roundToInt() * 16))


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/StreamableVideo.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.utils

import android.util.Log
import com.abedelazizshe.lightcompressorlibrary.data.*
import java.io.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.channels.FileChannel

object StreamableVideo {

    private const val tag = "StreamableVideo"
    private const val ATOM_PREAMBLE_SIZE = 8

    /**
     * @param in  Input file.
     * @param out Output file.
     * @return false if input file is already fast start.
     * @throws IOException
     */
    @Throws(IOException::class)
    fun start(`in`: File?, out: File): Boolean {
        var ret = false
        var inStream: FileInputStream? = null
        var outStream: FileOutputStream? = null
        return try {
            inStream = FileInputStream(`in`)
            val infile = inStream.channel
            outStream = FileOutputStream(out)
            val outfile = outStream.channel
            convert(infile, outfile).also { ret = it }
        } finally {
            safeClose(inStream)
            safeClose(outStream)
            if (!ret) {
                out.delete()
            }
        }
    }

    @Throws(IOException::class)
    private fun convert(infile: FileChannel, outfile: FileChannel): Boolean {
        val atomBytes = ByteBuffer.allocate(ATOM_PREAMBLE_SIZE).order(ByteOrder.BIG_ENDIAN)
        var atomType = 0
        var atomSize: Long = 0
        val lastOffset: Long
        val moovAtom: ByteBuffer
        var ftypAtom: ByteBuffer? = null
        var startOffset: Long = 0

        // traverse through the atoms in the file to make sure that 'moov' is at the end
        while (readAndFill(infile, atomBytes)) {
            atomSize = uInt32ToLong(atomBytes.int)
            atomType = atomBytes.int

            // keep ftyp atom
            if (atomType == FTYP_ATOM) {
                val ftypAtomSize = uInt32ToInt(atomSize)
                ftypAtom = ByteBuffer.allocate(ftypAtomSize).order(ByteOrder.BIG_ENDIAN)
                atomBytes.rewind()
                ftypAtom.put(atomBytes)
                if (infile.read(ftypAtom) < ftypAtomSize - ATOM_PREAMBLE_SIZE) break
                ftypAtom.flip()
                startOffset = infile.position() // after ftyp atom
            } else {
                if (atomSize == 1L) {
                    /* 64-bit special case */
                    atomBytes.clear()
                    if (!readAndFill(infile, atomBytes)) break
                    atomSize = uInt64ToLong(atomBytes.long)
                    infile.position(infile.position() + atomSize - ATOM_PREAMBLE_SIZE * 2) // seek
                } else {
                    infile.position(infile.position() + atomSize - ATOM_PREAMBLE_SIZE) // seek
                }
            }
            if (atomType != FREE_ATOM
                && atomType != JUNK_ATOM
                && atomType != MDAT_ATOM
                && atomType != MOOV_ATOM
                && atomType != PNOT_ATOM
                && atomType != SKIP_ATOM
                && atomType != WIDE_ATOM
                && atomType != PICT_ATOM
                && atomType != UUID_ATOM
                && atomType != FTYP_ATOM
            ) {
                Log.wtf(tag, "encountered non-QT top-level atom (is this a QuickTime file?)")
                break
            }

            /* The atom header is 8 (or 16 bytes), if the atom size (which
         * includes these 8 or 16 bytes) is less than that, we won't be
         * able to continue scanning sensibly after this atom, so break. */
            if (atomSize < 8) break
        }
        if (atomType != MOOV_ATOM) {
            Log.wtf(tag, "last atom in file was not a moov atom")
            return false
        }

        // atomSize is uint64, but for moov uint32 should be stored.
        val moovAtomSize: Int = uInt32ToInt(atomSize)
        lastOffset =
            infile.size() - moovAtomSize
        moovAtom = ByteBuffer.allocate(moovAtomSize).order(ByteOrder.BIG_ENDIAN)
        if (!readAndFill(infile, moovAtom, lastOffset)) {
            throw Exception("failed to read moov atom")
        }

        if (moovAtom.getInt(12) == CMOV_ATOM) {
            throw Exception("this utility does not support compressed moov atoms yet")
        }

        // crawl through the moov chunk in search of stco or co64 atoms
        while (moovAtom.remaining() >= 8) {
            val atomHead = moovAtom.position()
            atomType = moovAtom.getInt(atomHead + 4)
            if (!(atomType == STCO_ATOM || atomType == CO64_ATOM)) {
                moovAtom.position(moovAtom.position() + 1)
                continue
            }
            atomSize = uInt32ToLong(moovAtom.getInt(atomHead)) // uint32
            if (atomSize > moovAtom.remaining()) {
                throw Exception("bad atom size")
            }
            // skip size (4 bytes), type (4 bytes), version (1 byte) and flags (3 bytes)
            moovAtom.position(atomHead + 12)
            if (moovAtom.remaining() < 4) {
                throw Exception("malformed atom")
            }
            // uint32_t, but assuming moovAtomSize is in int32 range, so this will be in int32 range
            val offsetCount = uInt32ToInt(moovAtom.int)
            if (atomType == STCO_ATOM) {
                Log.i(tag, "patching stco atom...")
                if (moovAtom.remaining() < offsetCount * 4) {
                    throw Exception("bad atom size/element count")
                }
                for (i in 0 until offsetCount) {
                    val currentOffset = moovAtom.getInt(moovAtom.position())
                    val newOffset =
                        currentOffset + moovAtomSize // calculate uint32 in int, bitwise addition

                    if (currentOffset < 0 && newOffset >= 0) {
                        throw Exception(
                            "This is bug in original qt-faststart.c: "
                                    + "stco atom should be extended to co64 atom as new offset value overflows uint32, "
                                    + "but is not implemented."
                        )
                    }
                    moovAtom.putInt(newOffset)
                }
            } else if (atomType == CO64_ATOM) {
                Log.wtf(tag, "patching co64 atom...")
                if (moovAtom.remaining() < offsetCount * 8) {
                    throw Exception("bad atom size/element count")
                }
                for (i in 0 until offsetCount) {
                    val currentOffset = moovAtom.getLong(moovAtom.position())
                    moovAtom.putLong(currentOffset + moovAtomSize) // calculate uint64 in long, bitwise addition
                }
            }
        }
        infile.position(startOffset) // seek after ftyp atom
        if (ftypAtom != null) {
            // dump the same ftyp atom
            Log.i(tag, "writing ftyp atom...")
            ftypAtom.rewind()
            outfile.write(ftypAtom)
        }

        // dump the new moov atom
        Log.i(tag, "writing moov atom...")
        moovAtom.rewind()
        outfile.write(moovAtom)

        // copy the remainder of the infile, from offset 0 -> (lastOffset - startOffset) - 1
        Log.i(tag, "copying rest of file...")
        infile.transferTo(startOffset, lastOffset - startOffset, outfile)
        return true
    }

    private fun safeClose(closeable: Closeable?) {
        if (closeable != null) {
            try {
                closeable.close()
            } catch (e: IOException) {
                Log.wtf(tag, "Failed to close file: ")
            }
        }
    }

    @Throws(IOException::class)
    private fun readAndFill(infile: FileChannel, buffer: ByteBuffer): Boolean {
        buffer.clear()
        val size = infile.read(buffer)
        buffer.flip()
        return size == buffer.capacity()
    }

    @Throws(IOException::class)
    private fun readAndFill(infile: FileChannel, buffer: ByteBuffer, position: Long): Boolean {
        buffer.clear()
        val size = infile.read(buffer, position)
        buffer.flip()
        return size == buffer.capacity()
    }
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/InputSurface.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import android.opengl.*
import android.view.Surface

class InputSurface(surface: Surface?) {

    private val eglRecordableAndroid = 0x3142
    private val eglOpenGlES2Bit = 4
    private var mEGLDisplay: EGLDisplay? = null
    private var mEGLContext: EGLContext? = null
    private var mEGLSurface: EGLSurface? = null
    private var mSurface: Surface? = null

    init {
        if (surface == null) {
            throw NullPointerException()
        }
        mSurface = surface
        eglSetup()
    }

    private fun eglSetup() {
        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
        if (mEGLDisplay === EGL14.EGL_NO_DISPLAY) {
            throw RuntimeException("unable to get EGL14 display")
        }
        val version = IntArray(2)
        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            mEGLDisplay = null
            throw RuntimeException("unable to initialize EGL14")
        }
        val attribList = intArrayOf(
            EGL14.EGL_RED_SIZE,
            8,
            EGL14.EGL_GREEN_SIZE,
            8,
            EGL14.EGL_BLUE_SIZE,
            8,
            EGL14.EGL_RENDERABLE_TYPE,
            eglOpenGlES2Bit,
            eglRecordableAndroid,
            1,
            EGL14.EGL_NONE,
        )
        val configs = arrayOfNulls<EGLConfig>(1)
        val numConfigs = IntArray(1)
        if (!EGL14.eglChooseConfig(
                mEGLDisplay, attribList, 0, configs, 0, configs.size,
                numConfigs, 0
            )
        ) {
            throw RuntimeException("unable to find RGB888+recordable ES2 EGL config")
        }
        val attrs = intArrayOf(
            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL14.EGL_NONE
        )
        mEGLContext =
            EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrs, 0)
        checkEglError()
        if (mEGLContext == null) {
            throw RuntimeException("null context")
        }
        val surfaceAttrs = intArrayOf(
            EGL14.EGL_NONE
        )
        mEGLSurface = EGL14.eglCreateWindowSurface(
            mEGLDisplay, configs[0], mSurface,
            surfaceAttrs, 0
        )
        checkEglError()
        if (mEGLSurface == null) {
            throw RuntimeException("surface was null")
        }
    }

    fun release() {
        if (EGL14.eglGetCurrentContext() == mEGLContext) {
            EGL14.eglMakeCurrent(
                mEGLDisplay,
                EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT
            )
        }
        EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface)
        EGL14.eglDestroyContext(mEGLDisplay, mEGLContext)

        mSurface?.release()

        mEGLDisplay = null
        mEGLContext = null
        mEGLSurface = null

        mSurface = null
    }

    fun makeCurrent() {
        if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
            throw RuntimeException("eglMakeCurrent failed")
        }
    }

    fun swapBuffers(): Boolean =
        EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface)


    fun setPresentationTime(nsecs: Long) {
        EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs)
    }

    private fun checkEglError() {
        var failed = false
        while (EGL14.eglGetError() != EGL14.EGL_SUCCESS) {
            failed = true
        }
        if (failed) {
            throw RuntimeException("EGL error encountered (see log)")
        }
    }

}

================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/MP4Builder.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import android.media.MediaCodec
import android.media.MediaFormat
import com.coremedia.iso.boxes.*
import com.googlecode.mp4parser.util.Matrix
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.*

class MP4Builder {

    private lateinit var mdat: Mdat
    private lateinit var currentMp4Movie: Mp4Movie
    private lateinit var fos: FileOutputStream
    private lateinit var fc: FileChannel
    private var dataOffset: Long = 0
    private var wroteSinceLastMdat: Long = 0
    private var writeNewMdat = true
    private val track2SampleSizes = HashMap<Track, LongArray>()
    private lateinit var sizeBuffer: ByteBuffer

    @Throws(Exception::class)
    fun createMovie(mp4Movie: Mp4Movie): MP4Builder {
        currentMp4Movie = mp4Movie

        fos = FileOutputStream(mp4Movie.getCacheFile())
        fc = fos.channel

        val fileTypeBox: FileTypeBox = createFileTypeBox()
        fileTypeBox.getBox(fc)
        dataOffset += fileTypeBox.size
        wroteSinceLastMdat = dataOffset

        mdat = Mdat()
        sizeBuffer = ByteBuffer.allocateDirect(4)

        return this
    }

    @Throws(Exception::class)
    private fun flushCurrentMdat() {
        val oldPosition = fc.position()
        fc.position(mdat.offset)
        mdat.getBox(fc)
        fc.position(oldPosition)
        mdat.setDataOffset(0)
        mdat.setContentSize(0)
        fos.flush()
    }

    @Throws(Exception::class)
    fun writeSampleData(
        trackIndex: Int,
        byteBuf: ByteBuffer,
        bufferInfo: MediaCodec.BufferInfo,
        isAudio: Boolean
    ) {

        if (writeNewMdat) {
            mdat.apply {
                setContentSize(0)
                getBox(fc)
                setDataOffset(dataOffset)
            }
            dataOffset += 16
            wroteSinceLastMdat += 16
            writeNewMdat = false
        }

        mdat.setContentSize(mdat.getContentSize() + bufferInfo.size)
        wroteSinceLastMdat += bufferInfo.size.toLong()

        var flush = false
        if (wroteSinceLastMdat >= 32 * 1024) {
            flushCurrentMdat()
            writeNewMdat = true
            flush = true
            wroteSinceLastMdat = 0
        }

        currentMp4Movie.addSample(trackIndex, dataOffset, bufferInfo)

        if (!isAudio) {
            byteBuf.position(bufferInfo.offset + 4)
            byteBuf.limit(bufferInfo.offset + bufferInfo.size)

            sizeBuffer.position(0)
            sizeBuffer.putInt(bufferInfo.size - 4)
            sizeBuffer.position(0)
            fc.write(sizeBuffer)
        } else {
            byteBuf.position(bufferInfo.offset + 0)
            byteBuf.limit(bufferInfo.offset + bufferInfo.size)
        }

        fc.write(byteBuf)
        dataOffset += bufferInfo.size.toLong()

        if (flush) {
            fos.flush()
        }
    }

    fun addTrack(mediaFormat: MediaFormat, isAudio: Boolean): Int =
        currentMp4Movie.addTrack(mediaFormat, isAudio)

    @Throws(Exception::class)
    fun finishMovie() {
        if (mdat.getContentSize() != 0L) {
            flushCurrentMdat()
        }

        for (track in currentMp4Movie.getTracks()) {
            val samples: List<Sample> = track.getSamples()
            val sizes = LongArray(samples.size)
            for (i in sizes.indices) {
                sizes[i] = samples[i].size
            }
            track2SampleSizes[track] = sizes
        }

        val moov: Box = createMovieBox(currentMp4Movie)
        moov.getBox(fc)

        fos.flush()
        fc.close()
        fos.close()
    }

    private fun createFileTypeBox(): FileTypeBox {
        // completed list can be found at https://www.ftyps.com/
        val minorBrands = listOf(
            "isom", "iso2", "mp41"
        )

        return FileTypeBox("mp42", 0, minorBrands)
    }

    private fun gcd(a: Long, b: Long): Long {
        return if (b == 0L) a
        else gcd(b, a % b)
    }

    private fun getTimescale(mp4Movie: Mp4Movie): Long {
        var timescale: Long = 0
        if (mp4Movie.getTracks().isNotEmpty()) {
            timescale = mp4Movie.getTracks().iterator().next().getTimeScale().toLong()
        }

        for (track in mp4Movie.getTracks()) {
            timescale = gcd(
                track.getTimeScale().toLong(),
                timescale
            )
        }

        return timescale
    }

    private fun createMovieBox(movie: Mp4Movie): MovieBox {
        val movieBox = MovieBox()
        val mvhd = MovieHeaderBox()

        mvhd.apply {
            creationTime = Date()
            modificationTime = Date()
            matrix = Matrix.ROTATE_0
        }

        val movieTimeScale = getTimescale(movie)
        var duration: Long = 0

        for (track in movie.getTracks()) {
            val tracksDuration = track.getDuration() * movieTimeScale / track.getTimeScale()
            if (tracksDuration > duration) {
                duration = tracksDuration
            }
        }

        mvhd.duration = duration
        mvhd.timescale = movieTimeScale
        mvhd.nextTrackId = (movie.getTracks().size + 1).toLong()
        movieBox.addBox(mvhd)

        for (track in movie.getTracks()) {
            movieBox.addBox(createTrackBox(track, movie))
        }

        return movieBox
    }

    private fun createTrackBox(track: Track, movie: Mp4Movie): TrackBox {
        val trackBox = TrackBox()
        val tkhd = TrackHeaderBox()
        tkhd.apply {
            isEnabled = true
            isInPreview = true
            isInMovie = true
            matrix = if (track.isAudio()) {
                Matrix.ROTATE_0
            } else {
                movie.getMatrix()
            }
            alternateGroup = 0
            creationTime = track.getCreationTime()
            duration = track.getDuration() * getTimescale(movie) / track.getTimeScale()
            height = track.getHeight().toDouble()
            width = track.getWidth().toDouble()
            layer = 0
            modificationTime = Date()
            trackId = track.getTrackId() + 1
            volume = track.getVolume()
        }
        trackBox.addBox(tkhd)

        val mdia = MediaBox()
        trackBox.addBox(mdia)

        val mdhd = MediaHeaderBox()
        mdhd.apply {
            creationTime = track.getCreationTime()
            duration = track.getDuration()
            timescale = track.getTimeScale().toLong()
            language = "eng"
        }
        mdia.addBox(mdhd)

        val hdlr = HandlerBox()
        hdlr.apply {
            name = if (track.isAudio()) "SoundHandle" else "VideoHandle"
            handlerType = track.getHandler()
        }
        mdia.addBox(hdlr)

        val minf = MediaInformationBox()
        when {
            track.getHandler() == "vide" -> {
                minf.addBox(VideoMediaHeaderBox())
            }

            track.getHandler() == "soun" -> {
                minf.addBox(SoundMediaHeaderBox())
            }

            track.getHandler() == "text" -> {
                minf.addBox(NullMediaHeaderBox())
            }

            track.getHandler() == "subt" -> {
                minf.addBox(SubtitleMediaHeaderBox())
            }

            track.getHandler() == "hint" -> {
                minf.addBox(HintMediaHeaderBox())
            }

            track.getHandler() == "sbtl" -> {
                minf.addBox(NullMediaHeaderBox())
            }
        }

        val dinf = DataInformationBox()
        val dref = DataReferenceBox()
        dinf.addBox(dref)

        val url = DataEntryUrlBox()
        url.flags = 1

        dref.addBox(url)
        minf.addBox(dinf)

        val stbl: Box = createStbl(track)
        minf.addBox(stbl)
        mdia.addBox(minf)

        return trackBox
    }

    private fun createStbl(track: Track): Box {
        val stbl = SampleTableBox()
        createStsd(track, stbl)
        createStts(track, stbl)
        createStss(track, stbl)
        createStsc(track, stbl)
        createStsz(track, stbl)
        createStco(track, stbl)
        return stbl
    }

    private fun createStsd(track: Track, stbl: SampleTableBox) {
        stbl.addBox(track.getSampleDescriptionBox())
    }

    private fun createStts(track: Track, stbl: SampleTableBox) {
        var lastEntry: TimeToSampleBox.Entry? = null
        val entries: MutableList<TimeToSampleBox.Entry> = ArrayList()
        for (delta in track.getSampleDurations()) {
            if (lastEntry != null && lastEntry.delta == delta) {
                lastEntry.count = lastEntry.count + 1
            } else {
                lastEntry = TimeToSampleBox.Entry(1, delta)
                entries.add(lastEntry)
            }
        }
        val stts = TimeToSampleBox()
        stts.entries = entries
        stbl.addBox(stts)
    }

    private fun createStss(track: Track, stbl: SampleTableBox) {
        val syncSamples = track.getSyncSamples()
        if (syncSamples != null && syncSamples.isNotEmpty()) {
            val stss = SyncSampleBox()
            stss.sampleNumber = syncSamples
            stbl.addBox(stss)
        }
    }

    private fun createStsc(track: Track, stbl: SampleTableBox) {
        val stsc = SampleToChunkBox()
        stsc.entries = LinkedList()

        var lastOffset: Long
        var lastChunkNumber = 1
        var lastSampleCount = 0
        var previousWrittenChunkCount = -1

        val samplesCount = track.getSamples().size
        for (a in 0 until samplesCount) {
            val sample = track.getSamples()[a]
            val offset = sample.offset
            val size = sample.size

            lastOffset = offset + size
            lastSampleCount++

            var write = false
            if (a != samplesCount - 1) {
                val nextSample = track.getSamples()[a + 1]
                if (lastOffset != nextSample.offset) {
                    write = true
                }
            } else {
                write = true
            }

            if (write) {
                if (previousWrittenChunkCount != lastSampleCount) {
                    stsc.entries.add(
                        SampleToChunkBox.Entry(
                            lastChunkNumber.toLong(),
                            lastSampleCount.toLong(), 1
                        )
                    )
                    previousWrittenChunkCount = lastSampleCount
                }
                lastSampleCount = 0
                lastChunkNumber++
            }
        }
        stbl.addBox(stsc)
    }

    private fun createStsz(track: Track, stbl: SampleTableBox) {
        val stsz = SampleSizeBox()
        stsz.sampleSizes = track2SampleSizes[track]
        stbl.addBox(stsz)
    }

    private fun createStco(track: Track, stbl: SampleTableBox) {
        val chunksOffsets = ArrayList<Long>()
        var lastOffset: Long = -1
        for (sample in track.getSamples()) {
            val offset = sample.offset
            if (lastOffset != -1L && lastOffset != offset) {
                lastOffset = -1
            }
            if (lastOffset == -1L) {
                chunksOffsets.add(offset)
            }
            lastOffset = offset + sample.size
        }
        val chunkOffsetsLong = LongArray(chunksOffsets.size)
        for (a in chunksOffsets.indices) {
            chunkOffsetsLong[a] = chunksOffsets[a]
        }
        val stco = StaticChunkOffsetBox()
        stco.chunkOffsets = chunkOffsetsLong
        stbl.addBox(stco)
    }
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Mdat.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import com.coremedia.iso.BoxParser
import com.coremedia.iso.IsoFile
import com.coremedia.iso.IsoTypeWriter
import com.coremedia.iso.boxes.Box
import com.coremedia.iso.boxes.Container
import com.googlecode.mp4parser.DataSource
import java.nio.ByteBuffer
import java.nio.channels.WritableByteChannel

class Mdat : Box {

    private lateinit var parent: Container
    private var contentSize = (1024 * 1024 * 1024).toLong()
    private var dataOffset: Long = 0

    override fun getParent(): Container = parent

    override fun setParent(parent: Container) {
        this.parent = parent
    }

    override fun getSize(): Long = 16 + contentSize

    override fun getOffset(): Long = dataOffset

    fun setDataOffset(offset: Long) {
        dataOffset = offset
    }

    fun setContentSize(contentSize: Long) {
        this.contentSize = contentSize
    }

    fun getContentSize(): Long {
        return contentSize
    }

    override fun getType(): String = "mdat"

    private fun isSmallBox(contentSize: Long): Boolean = contentSize + 8 < 4294967296L

    override fun getBox(writableByteChannel: WritableByteChannel) {
        val bb = ByteBuffer.allocate(16)
        val size = size
        if (isSmallBox(size)) {
            if (size >= 0 && size <= 1L shl 32) {
                IsoTypeWriter.writeUInt32(bb, size)
            } else {
                // TODO(ABED): Investigate when this could happen.
                IsoTypeWriter.writeUInt32(bb, 1)
            }
        } else {
            IsoTypeWriter.writeUInt32(bb, 1)
        }
        bb.put(IsoFile.fourCCtoBytes("mdat"))
        if (isSmallBox(size)) {
            bb.put(ByteArray(8))
        } else {
            IsoTypeWriter.writeUInt64(bb, if (size >= 0) size else 1)
        }
        bb.rewind()
        writableByteChannel.write(bb)
    }

    override fun parse(
        dataSource: DataSource?,
        header: ByteBuffer?,
        contentSize: Long,
        boxParser: BoxParser?
    ) {
    }
}

================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Mp4Movie.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import android.media.MediaCodec
import android.media.MediaFormat
import com.googlecode.mp4parser.util.Matrix
import java.io.File
import java.util.*

class Mp4Movie {

    private var matrix = Matrix.ROTATE_0
    private val tracks = ArrayList<Track>()
    private var cacheFile: File? = null

    fun getMatrix(): Matrix? = matrix

    fun setCacheFile(file: File) {
        cacheFile = file
    }

    fun setRotation(angle: Int) {
        when (angle) {
            0 -> {
                matrix = Matrix.ROTATE_0
            }
            90 -> {
                matrix = Matrix.ROTATE_90
            }
            180 -> {
                matrix = Matrix.ROTATE_180
            }
            270 -> {
                matrix = Matrix.ROTATE_270
            }
        }
    }

    fun getTracks(): ArrayList<Track> = tracks

    fun getCacheFile(): File? = cacheFile

    fun addSample(trackIndex: Int, offset: Long, bufferInfo: MediaCodec.BufferInfo) {
        if (trackIndex < 0 || trackIndex >= tracks.size) {
            return
        }
        val track = tracks[trackIndex]
        track.addSample(offset, bufferInfo)
    }

    fun addTrack(mediaFormat: MediaFormat, isAudio: Boolean): Int {
        tracks.add(Track(tracks.size, mediaFormat, isAudio))
        return tracks.size - 1
    }
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/OutputSurface.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import android.graphics.SurfaceTexture
import android.graphics.SurfaceTexture.OnFrameAvailableListener
import android.view.Surface

class OutputSurface : OnFrameAvailableListener {

    private var mSurfaceTexture: SurfaceTexture? = null
    private var mSurface: Surface? = null
    private val mFrameSyncObject = Object()
    private var mFrameAvailable = false
    private var mTextureRender: TextureRenderer? = null

    /**
     * Creates an OutputSurface using the current EGL context. This Surface will be
     * passed to MediaCodec.configure().
     */
    init {
        setup()
    }

    /**
     * Creates instances of TextureRender and SurfaceTexture, and a Surface associated
     * with the SurfaceTexture.
     */
    private fun setup() {
        mTextureRender = TextureRenderer()
        mTextureRender?.let {
            it.surfaceCreated()

            // Even if we don't access the SurfaceTexture after the constructor returns, we
            // still need to keep a reference to it. The Surface doesn't retain a reference
            // at the Java level, so if we don't either then the object can get GCed, which
            // causes the native finalizer to run.
            mSurfaceTexture = SurfaceTexture(it.getTextureId())
            mSurfaceTexture?.let { surfaceTexture ->
                surfaceTexture.setOnFrameAvailableListener(this)
                mSurface = Surface(mSurfaceTexture)
            }
        }
    }

    /**
     * Discards all resources held by this class, notably the EGL context.
     */
    fun release() {
        mSurface?.release()

        mTextureRender = null
        mSurface = null
        mSurfaceTexture = null
    }

    /**
     * Returns the Surface that we draw onto.
     */
    fun getSurface(): Surface? = mSurface

    /**
     * Latches the next buffer into the texture.  Must be called from the thread that created
     * the OutputSurface object, after the onFrameAvailable callback has signaled that new
     * data is available.
     */
    fun awaitNewImage() {
        val timeOutMS = 100
        synchronized(mFrameSyncObject) {
            while (!mFrameAvailable) {
                try {
                    // Wait for onFrameAvailable() to signal us.  Use a timeout to avoid
                    // stalling the test if it doesn't arrive.
                    mFrameSyncObject.wait(timeOutMS.toLong())
                    if (!mFrameAvailable) {
                        throw RuntimeException("Surface frame wait timed out")
                    }
                } catch (ie: InterruptedException) {
                    throw RuntimeException(ie)
                }
            }
            mFrameAvailable = false
        }
        mTextureRender?.checkGlError("before updateTexImage")
        mSurfaceTexture?.updateTexImage()
    }

    /**
     * Draws the data from SurfaceTexture onto the current EGL surface.
     */
    fun drawImage() {
        mTextureRender?.drawFrame(mSurfaceTexture!!)
    }

    override fun onFrameAvailable(p0: SurfaceTexture?) {
        synchronized(mFrameSyncObject) {
            if (mFrameAvailable) {
                throw RuntimeException("mFrameAvailable already set, frame could be dropped")
            }
            mFrameAvailable = true
            mFrameSyncObject.notifyAll()
        }
    }
}


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

data class Result(
    val index: Int,
    val success: Boolean,
    val failureMessage: String?,
    val size: Long = 0,
    val path: String? = null,
)


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Sample.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

data class Sample(var offset: Long, var size: Long)


================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/TextureRenderer.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import android.graphics.SurfaceTexture
import android.opengl.GLES11Ext
import android.opengl.GLES20
import android.opengl.Matrix
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer

class TextureRenderer {

    private val floatSizeBytes = 4
    private val triangleVerticesDataStrideBytes = 5 * floatSizeBytes
    private val triangleVerticesDataPosOffset = 0
    private val triangleVerticesDataUvOffset = 3
    private var mTriangleVertices: FloatBuffer

    private val vertexShader = """uniform mat4 uMVPMatrix;
uniform mat4 uSTMatrix;
attribute vec4 aPosition;
attribute vec4 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
  gl_Position = uMVPMatrix * aPosition;
  vTextureCoord = (uSTMatrix * aTextureCoord).xy;
}
"""

    private val fragmentShader = """#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTextureCoord;
uniform samplerExternalOES sTexture;
void main() {
  gl_FragColor = texture2D(sTexture, vTextureCoord);
}
"""

    private val mMVPMatrix = FloatArray(16)
    private val mSTMatrix = FloatArray(16)

    private var mProgram = 0
    private var mTextureID = -12345
    private var muMVPMatrixHandle = 0
    private var muSTMatrixHandle = 0
    private var maPositionHandle = 0
    private var maTextureHandle = 0

    init {
        val mTriangleVerticesData = floatArrayOf( // X, Y, Z, U, V
            -1.0f, -1.0f, 0f, 0f, 0f,
            1.0f, -1.0f, 0f, 1f, 0f,
            -1.0f, 1.0f, 0f, 0f, 1f,
            1.0f, 1.0f, 0f, 1f, 1f
        )
        mTriangleVertices = ByteBuffer.allocateDirect(
            mTriangleVerticesData.size * floatSizeBytes
        )
            .order(ByteOrder.nativeOrder()).asFloatBuffer()
        mTriangleVertices.put(mTriangleVerticesData).position(0)

        Matrix.setIdentityM(mSTMatrix, 0)
    }

    fun getTextureId(): Int = mTextureID

    fun drawFrame(st: SurfaceTexture) {
        checkGlError("onDrawFrame start")
        st.getTransformMatrix(mSTMatrix)

        GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f)
        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT or GLES20.GL_COLOR_BUFFER_BIT)

        GLES20.glUseProgram(mProgram)
        checkGlError("glUseProgram")

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID)

        mTriangleVertices.position(triangleVerticesDataPosOffset)
        GLES20.glVertexAttribPointer(
            maPositionHandle, 3, GLES20.GL_FLOAT, false,
            triangleVerticesDataStrideBytes, mTriangleVertices
        )
        checkGlError("glVertexAttribPointer maPosition")
        GLES20.glEnableVertexAttribArray(maPositionHandle)
        checkGlError("glEnableVertexAttribArray maPositionHandle")

        mTriangleVertices.position(triangleVerticesDataUvOffset)
        GLES20.glVertexAttribPointer(
            maTextureHandle, 2, GLES20.GL_FLOAT, false,
            triangleVerticesDataStrideBytes, mTriangleVertices
        )
        checkGlError("glVertexAttribPointer maTextureHandle")
        GLES20.glEnableVertexAttribArray(maTextureHandle)
        checkGlError("glEnableVertexAttribArray maTextureHandle")

        Matrix.setIdentityM(mMVPMatrix, 0)
        GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0)
        GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0)

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
        checkGlError("glDrawArrays")
        GLES20.glFinish()
    }

    /**
     * Initializes GL state.  Call this after the EGL surface has been created and made current.
     */
    fun surfaceCreated() {
        mProgram = createProgram()
        if (mProgram == 0) {
            throw RuntimeException("failed creating program")
        }
        maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition")
        checkGlError("glGetAttribLocation aPosition")
        if (maPositionHandle == -1) {
            throw RuntimeException("Could not get attrib location for aPosition")
        }
        maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord")
        checkGlError("glGetAttribLocation aTextureCoord")
        if (maTextureHandle == -1) {
            throw RuntimeException("Could not get attrib location for aTextureCoord")
        }

        muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")
        checkGlError("glGetUniformLocation uMVPMatrix")
        if (muMVPMatrixHandle == -1) {
            throw RuntimeException("Could not get attrib location for uMVPMatrix")
        }

        muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix")
        checkGlError("glGetUniformLocation uSTMatrix")
        if (muSTMatrixHandle == -1) {
            throw RuntimeException("Could not get attrib location for uSTMatrix")
        }

        val textures = IntArray(1)
        GLES20.glGenTextures(1, textures, 0)
        mTextureID = textures[0]
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID)
        checkGlError("glBindTexture mTextureID")

        GLES20.glTexParameterf(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
            GLES20.GL_NEAREST.toFloat()
        )
        GLES20.glTexParameterf(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
            GLES20.GL_LINEAR.toFloat()
        )
        GLES20.glTexParameteri(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
            GLES20.GL_CLAMP_TO_EDGE
        )
        GLES20.glTexParameteri(
            GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
            GLES20.GL_CLAMP_TO_EDGE
        )
        checkGlError("glTexParameter")
    }

    private fun loadShader(shaderType: Int, source: String): Int {
        var shader = GLES20.glCreateShader(shaderType)
        checkGlError("glCreateShader type=$shaderType")
        GLES20.glShaderSource(shader, source)
        GLES20.glCompileShader(shader)
        val compiled = IntArray(1)
        GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0)
        if (compiled[0] == 0) {
            GLES20.glDeleteShader(shader)
            shader = 0
        }
        return shader
    }

    private fun createProgram(): Int {
        val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShader)
        if (vertexShader == 0) {
            return 0
        }

        val pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader)
        if (pixelShader == 0) {
            return 0
        }

        var program = GLES20.glCreateProgram()
        checkGlError("glCreateProgram")
        if (program == 0) {
            return 0
        }
        GLES20.glAttachShader(program, vertexShader)
        checkGlError("glAttachShader")
        GLES20.glAttachShader(program, pixelShader)
        checkGlError("glAttachShader")
        GLES20.glLinkProgram(program)
        val linkStatus = IntArray(1)
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0)
        if (linkStatus[0] != GLES20.GL_TRUE) {
            GLES20.glDeleteProgram(program)
            program = 0
        }
        return program
    }

    fun checkGlError(op: String) {
        var error: Int
        if (GLES20.glGetError().also { error = it } != GLES20.GL_NO_ERROR) {
            throw RuntimeException("$op: glError $error")
        }
    }

}

================================================
FILE: lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Track.kt
================================================
package com.abedelazizshe.lightcompressorlibrary.video

import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import com.coremedia.iso.boxes.SampleDescriptionBox
import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry
import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry
import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.SLConfigDescriptor
import com.mp4parser.iso14496.part15.AvcConfigurationBox
import java.util.*

class Track(id: Int, format: MediaFormat, audio: Boolean) {

    private var trackId: Long = 0
    private val samples = ArrayList<Sample>()
    private var duration: Long = 0
    private var handler: String
    private var sampleDescriptionBox: SampleDescriptionBox
    private var syncSamples: LinkedList<Int>? = null
    private var timeScale = 0
    private val creationTime = Date()
    private var height = 0
    private var width = 0
    private var volume = 0f
    private val sampleDurations = ArrayList<Long>()
    private val isAudio = audio
    private var samplingFrequencyIndexMap: Map<Int, Int> = HashMap()
    private var lastPresentationTimeUs: Long = 0
    private var first = true

    init {
        samplingFrequencyIndexMap = mapOf(
            96000 to 0x0,
            88200 to 0x1,
            64000 to 0x2,
            48000 to 0x3,
            44100 to 0x4,
            32000 to 0x5,
            24000 to 0x6,
            22050 to 0x7,
            16000 to 0x8,
            12000 to 0x9,
            11025 to 0xa,
            8000 to 0xb,
        )

        trackId = id.toLong()
        if (!isAudio) {
            sampleDurations.add(3015.toLong())
            duration = 3015
            width = format.getInteger(MediaFormat.KEY_WIDTH)
            height = format.getInteger(MediaFormat.KEY_HEIGHT)
            timeScale = 90000
            syncSamples = LinkedList()
            handler = "vide"

            sampleDescriptionBox = SampleDescriptionBox()
            val mime = format.getString(MediaFormat.KEY_MIME)
            if (mime == "video/avc") {
                val visualSampleEntry =
                    VisualSampleEntry(VisualSampleEntry.TYPE3).setup(width, height)

                val avcConfigurationBox = AvcConfigurationBox()
                if (format.getByteBuffer("csd-0") != null) {
                    val spsArray = ArrayList<ByteArray>()
                    val spsBuff = format.getByteBuffer("csd-0")
                    spsBuff!!.position(4)

                    val spsBytes = ByteArray(spsBuff.remaining())
                    spsBuff[spsBytes]
                    spsArray.add(spsBytes)

                    val ppsArray = ArrayList<ByteArray>()
                    val ppsBuff = format.getByteBuffer("csd-1")
                    ppsBuff?.let {
                        it.position(4)

                        val ppsBytes = ByteArray(it.remaining())
                        it[ppsBytes]

                        ppsArray.add(ppsBytes)
                        avcConfigurationBox.sequenceParameterSets = spsArray
                        avcConfigurationBox.pictureParameterSets = ppsArray
                    }
                }

                if (format.containsKey("level")) {
                    when (format.getInteger("level")) {
                        MediaCodecInfo.CodecProfileLevel.AVCLevel1 -> {
                            avcConfigurationBox.avcLevelIndication = 1
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel2 -> {
                            avcConfigurationBox.avcLevelIndication = 2
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel11 -> {
                            avcConfigurationBox.avcLevelIndication = 11
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel12 -> {
                            avcConfigurationBox.avcLevelIndication = 12
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel13 -> {
                            avcConfigurationBox.avcLevelIndication = 13
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel21 -> {
                            avcConfigurationBox.avcLevelIndication = 21
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel22 -> {
                            avcConfigurationBox.avcLevelIndication = 22
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel3 -> {
                            avcConfigurationBox.avcLevelIndication = 3
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel31 -> {
                            avcConfigurationBox.avcLevelIndication = 31
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel32 -> {
                            avcConfigurationBox.avcLevelIndication = 32
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel4 -> {
                            avcConfigurationBox.avcLevelIndication = 4
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel41 -> {
                            avcConfigurationBox.avcLevelIndication = 41
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel42 -> {
                            avcConfigurationBox.avcLevelIndication = 42
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel5 -> {
                            avcConfigurationBox.avcLevelIndication = 5
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel51 -> {
                            avcConfigurationBox.avcLevelIndication = 51
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel52 -> {
                            avcConfigurationBox.avcLevelIndication = 52
                        }
                        MediaCodecInfo.CodecProfileLevel.AVCLevel1b -> {
                            avcConfigurationBox.avcLevelIndication = 0x1b
                        }
                        else -> avcConfigurationBox.avcLevelIndication = 13
                    }
                } else {
                    avcConfigurationBox.avcLevelIndication = 13
                }

                avcConfigurationBox.avcProfileIndication = 100
                avcConfigurationBox.bitDepthLumaMinus8 = -1
                avcConfigurationBox.bitDepthChromaMinus8 = -1
                avcConfigurationBox.chromaFormat = -1
                avcConfigurationBox.configurationVersion = 1
                avcConfigurationBox.lengthSizeMinusOne = 3
                avcConfigurationBox.profileCompatibility = 0

                visualSampleEntry.addBox(avcC
Download .txt
gitextract_wb5gjwc9/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .idea/
│   ├── .name
│   ├── codeStyles/
│   │   ├── Project.xml
│   │   └── codeStyleConfig.xml
│   ├── compiler.xml
│   ├── dictionaries/
│   │   └── abdsh.xml
│   ├── gradle.xml
│   ├── inspectionProfiles/
│   │   └── Project_Default.xml
│   ├── jarRepositories.xml
│   ├── kotlinc.xml
│   ├── misc.xml
│   └── vcs.xml
├── LICENSE
├── README.md
├── app/
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── abedelazizshe/
│       │               └── lightcompressor/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── abedelazizshe/
│       │   │           └── lightcompressor/
│       │   │               ├── MainActivity.kt
│       │   │               ├── RecyclerViewAdapter.kt
│       │   │               ├── Utils.kt
│       │   │               ├── VideoDetailsModel.kt
│       │   │               └── VideoPlayerActivity.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_launcher_background.xml
│       │       │   ├── ic_play_white_24dp.xml
│       │       │   └── ic_video_library_white_24dp.xml
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_video_player.xml
│       │       │   ├── content_main.xml
│       │       │   └── recycler_view_item.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       └── xml/
│       │           └── media_capabilities.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── abedelazizshe/
│                       └── lightcompressor/
│                           └── ExampleUnitTest.kt
├── build.gradle
├── gradle/
│   └── wrapper/
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── lightcompressor/
│   ├── .idea/
│   │   └── .gitignore
│   ├── build.gradle
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── abedelazizshe/
│       │               └── lightcompressorlibrary/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── abedelazizshe/
│       │   │           └── lightcompressorlibrary/
│       │   │               ├── CompressionInterface.kt
│       │   │               ├── VideoCompressor.kt
│       │   │               ├── compressor/
│       │   │               │   └── Compressor.kt
│       │   │               ├── config/
│       │   │               │   ├── Configuration.kt
│       │   │               │   └── VideoResizer.kt
│       │   │               ├── data/
│       │   │               │   └── Atoms.kt
│       │   │               ├── utils/
│       │   │               │   ├── CompressorUtils.kt
│       │   │               │   ├── FileUtils.kt
│       │   │               │   ├── NumbersUtils.kt
│       │   │               │   └── StreamableVideo.kt
│       │   │               └── video/
│       │   │                   ├── InputSurface.kt
│       │   │                   ├── MP4Builder.kt
│       │   │                   ├── Mdat.kt
│       │   │                   ├── Mp4Movie.kt
│       │   │                   ├── OutputSurface.kt
│       │   │                   ├── Result.kt
│       │   │                   ├── Sample.kt
│       │   │                   ├── TextureRenderer.kt
│       │   │                   └── Track.kt
│       │   └── res/
│       │       └── values/
│       │           └── strings.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── abedelazizshe/
│                       └── lightcompressorlibrary/
│                           └── ExampleUnitTest.kt
└── settings.gradle
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (207K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 700,
    "preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
  },
  {
    "path": ".gitignore",
    "chars": 379,
    "preview": ".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/librari"
  },
  {
    "path": ".idea/.name",
    "chars": 15,
    "preview": "VideoCompressor"
  },
  {
    "path": ".idea/codeStyles/Project.xml",
    "chars": 3567,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <code_scheme name=\"Project\" version=\"173\">\n    <JetCodeStyleSettings>"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "chars": 142,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
  },
  {
    "path": ".idea/compiler.xml",
    "chars": 169,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"CompilerConfiguration\">\n    <bytecodeTar"
  },
  {
    "path": ".idea/dictionaries/abdsh.xml",
    "chars": 196,
    "preview": "<component name=\"ProjectDictionaryState\">\n  <dictionary name=\"abdsh\">\n    <words>\n      <w>ftyp</w>\n      <w>mdat</w>\n  "
  },
  {
    "path": ".idea/gradle.xml",
    "chars": 757,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"GradleMigrationSettings\" migrationVersio"
  },
  {
    "path": ".idea/inspectionProfiles/Project_Default.xml",
    "chars": 1444,
    "preview": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project De"
  },
  {
    "path": ".idea/jarRepositories.xml",
    "chars": 1455,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"RemoteRepositoriesConfiguration\">\n    <r"
  },
  {
    "path": ".idea/kotlinc.xml",
    "chars": 176,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"KotlinJpsPluginSettings\">\n    <option na"
  },
  {
    "path": ".idea/misc.xml",
    "chars": 1167,
    "preview": "<project version=\"4\">\n  <component name=\"CMakeSettings\">\n    <configurations>\n      <configuration PROFILE_NAME=\"Debug\" "
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 180,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 9723,
    "preview": "[![JitPack](https://jitpack.io/v/AbedElazizShe/LightCompressor.svg)](https://jitpack.io/#AbedElazizShe/LightCompressor)\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 1813,
    "preview": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-kapt'\n\nandroid {\n    compil"
  },
  {
    "path": "app/src/androidTest/java/com/abedelazizshe/lightcompressor/ExampleInstrumentedTest.kt",
    "chars": 694,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.tes"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 1737,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/MainActivity.kt",
    "chars": 8378,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/RecyclerViewAdapter.kt",
    "chars": 2382,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport andr"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/Utils.kt",
    "chars": 3964,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport android.content.Context\nimport android.database.Cursor\nimport android."
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/VideoDetailsModel.kt",
    "chars": 207,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport android.net.Uri\n\ndata class VideoDetailsModel(\n    val playableVideoPa"
  },
  {
    "path": "app/src/main/java/com/abedelazizshe/lightcompressor/VideoPlayerActivity.kt",
    "chars": 2676,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport android.content.Context\nimport android.content.Intent\nimport android.n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 5606,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_white_24dp.xml",
    "chars": 411,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:width=\"50dp\"\n\tandroid:height=\"50dp\"\n\tandroid"
  },
  {
    "path": "app/src/main/res/drawable/ic_video_library_white_24dp.xml",
    "chars": 436,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    "
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 1880,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 3948,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "app/src/main/res/layout/activity_video_player.xml",
    "chars": 775,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "app/src/main/res/layout/content_main.xml",
    "chars": 1338,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas."
  },
  {
    "path": "app/src/main/res/layout/recycler_view_item.xml",
    "chars": 2855,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas."
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 272,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 272,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 393,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#344772</color>\n    <color name=\"color"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 67,
    "preview": "<resources>\n    <dimen name=\"fab_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 245,
    "preview": "<resources>\n    <string name=\"app_name\">VideoCompressor</string>\n    <string name=\"home_title\">Video Compressor Sample</"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 791,
    "preview": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.MaterialComponents.Light.NoAc"
  },
  {
    "path": "app/src/main/res/xml/media_capabilities.xml",
    "chars": 471,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<media-capabilities xmlns:android=\"http://schemas.android.com/apk/res/android\">\n "
  },
  {
    "path": "app/src/test/java/com/abedelazizshe/lightcompressor/ExampleUnitTest.kt",
    "chars": 358,
    "preview": "package com.abedelazizshe.lightcompressor\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit t"
  },
  {
    "path": "build.gradle",
    "chars": 294,
    "preview": "buildscript {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 229,
    "preview": "#Sat Jan 25 15:17:42 SGT 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
  },
  {
    "path": "gradle.properties",
    "chars": 348,
    "preview": "android.enableJetifier=true\nandroid.injected.testOnly=false\nandroid.lifecycleProcessor.incremental=true\nandroid.useAndro"
  },
  {
    "path": "gradlew",
    "chars": 5296,
    "preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n##  Gradle start up"
  },
  {
    "path": "gradlew.bat",
    "chars": 2176,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
  },
  {
    "path": "jitpack.yml",
    "chars": 18,
    "preview": "jdk:\n  - openjdk11"
  },
  {
    "path": "lightcompressor/.idea/.gitignore",
    "chars": 38,
    "preview": "# Default ignored files\n/workspace.xml"
  },
  {
    "path": "lightcompressor/build.gradle",
    "chars": 1361,
    "preview": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\napply plugin: 'maven-publish'\n\nandroid {\n    compileS"
  },
  {
    "path": "lightcompressor/src/androidTest/java/com/abedelazizshe/lightcompressorlibrary/ExampleInstrumentedTest.kt",
    "chars": 713,
    "preview": "package com.abedelazizshe.lightcompressorlibrary\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport andro"
  },
  {
    "path": "lightcompressor/src/main/AndroidManifest.xml",
    "chars": 64,
    "preview": "<manifest package=\"com.abedelazizshe.lightcompressorlibrary\" />\n"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/CompressionInterface.kt",
    "chars": 693,
    "preview": "package com.abedelazizshe.lightcompressorlibrary\n\nimport androidx.annotation.MainThread\nimport androidx.annotation.Worke"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/VideoCompressor.kt",
    "chars": 9899,
    "preview": "package com.abedelazizshe.lightcompressorlibrary\n\nimport android.content.ContentValues\nimport android.content.Context\nim"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/compressor/Compressor.kt",
    "chars": 22788,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.compressor\n\nimport android.content.Context\nimport android.media.*\nimpor"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/Configuration.kt",
    "chars": 5923,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.config\n\nimport android.content.Context\nimport android.net.Uri\nimport an"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/config/VideoResizer.kt",
    "chars": 3781,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.config\n\nimport com.abedelazizshe.lightcompressorlibrary.utils.Compresso"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/data/Atoms.kt",
    "chars": 3861,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.data\n\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\n\n/*\nFOURCC i"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/CompressorUtils.kt",
    "chars": 10673,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport android.media.MediaCodecInfo\nimport android.media.MediaCo"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/FileUtils.kt",
    "chars": 1857,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport android.content.ContentValues\nimport android.content.Cont"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/NumbersUtils.kt",
    "chars": 743,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport kotlin.math.roundToInt\n\nfun uInt32ToLong(int32: Int): Lon"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/utils/StreamableVideo.kt",
    "chars": 8141,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.utils\n\nimport android.util.Log\nimport com.abedelazizshe.lightcompressor"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/InputSurface.kt",
    "chars": 3589,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.opengl.*\nimport android.view.Surface\n\nclass Input"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/MP4Builder.kt",
    "chars": 11555,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.media.MediaCodec\nimport android.media.MediaFormat"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Mdat.kt",
    "chars": 2036,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport com.coremedia.iso.BoxParser\nimport com.coremedia.iso.IsoF"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Mp4Movie.kt",
    "chars": 1358,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.media.MediaCodec\nimport android.media.MediaFormat"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/OutputSurface.kt",
    "chars": 3384,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.graphics.SurfaceTexture\nimport android.graphics.S"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Result.kt",
    "chars": 210,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\ndata class Result(\n    val index: Int,\n    val success: Boolean,"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Sample.kt",
    "chars": 108,
    "preview": "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",
    "chars": 7498,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.graphics.SurfaceTexture\nimport android.opengl.GLE"
  },
  {
    "path": "lightcompressor/src/main/java/com/abedelazizshe/lightcompressorlibrary/video/Track.kt",
    "chars": 11441,
    "preview": "package com.abedelazizshe.lightcompressorlibrary.video\n\nimport android.media.MediaCodec\nimport android.media.MediaCodecI"
  },
  {
    "path": "lightcompressor/src/main/res/values/strings.xml",
    "chars": 85,
    "preview": "<resources>\n    <string name=\"app_name\">LightCompressorLibrary</string>\n</resources>\n"
  },
  {
    "path": "lightcompressor/src/test/java/com/abedelazizshe/lightcompressorlibrary/ExampleUnitTest.kt",
    "chars": 365,
    "preview": "package com.abedelazizshe.lightcompressorlibrary\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local"
  },
  {
    "path": "settings.gradle",
    "chars": 277,
    "preview": "dependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n   "
  }
]

About this extraction

This page contains the full source code of the AbedElazizShe/LightCompressor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (189.6 KB), approximately 46.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!