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
================================================
[](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

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
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": "[](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.