├── .gitignore ├── LICENSE ├── README.md ├── Screenshots ├── Screenshot_2023-08-26-16-58-44-339_com.video.sample (1).jpg ├── linkedin.png ├── ss3.gif ├── twitter.png ├── ve1.png └── ve2.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── sample-app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── fonts │ │ ├── Inter-UI-Black.otf │ │ ├── Inter-UI-BlackItalic.otf │ │ ├── Inter-UI-Bold.otf │ │ ├── Inter-UI-BoldItalic.otf │ │ ├── Inter-UI-ExtraBold.otf │ │ ├── Inter-UI-ExtraBoldItalic.otf │ │ ├── Inter-UI-ExtraLight-BETA.otf │ │ ├── Inter-UI-ExtraLightItalic-BETA.otf │ │ ├── Inter-UI-Italic.otf │ │ ├── Inter-UI-Light-BETA.otf │ │ ├── Inter-UI-LightItalic-BETA.otf │ │ ├── Inter-UI-Medium.otf │ │ ├── Inter-UI-MediumItalic.otf │ │ ├── Inter-UI-Regular.otf │ │ ├── Inter-UI-SemiBold.otf │ │ ├── Inter-UI-SemiBoldItalic.otf │ │ ├── Inter-UI-Thin-BETA.otf │ │ └── Inter-UI-ThinItalic-BETA.otf │ ├── java │ └── com │ │ └── video │ │ └── sample │ │ ├── EditorActivity.kt │ │ ├── FontsConstants.kt │ │ ├── FontsHelper.kt │ │ ├── MainActivity.kt │ │ ├── PermissionsDialog.kt │ │ ├── RunOnUiThread.kt │ │ ├── VideoProgressDialog.kt │ │ └── VideoProgressIndeterminateDialog.kt │ └── res │ ├── drawable-v21 │ └── background_button.xml │ ├── drawable │ ├── back_white.xml │ ├── background_button.xml │ ├── dialog_rounded_bg.xml │ ├── ic_crop.xml │ ├── ic_proceed_error.xml │ ├── ic_trim.xml │ ├── ic_videocam_black_24dp.xml │ └── textview_blue_round.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_video_editor.xml │ ├── dialog_permissions.xml │ ├── progress_loading.xml │ └── progress_loading_indeterminate.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── provider_paths.xml ├── settings.gradle └── video-editor ├── build.gradle └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── video │ └── trimmer │ ├── interfaces │ ├── OnProgressVideoListener.kt │ ├── OnRangeSeekBarListener.kt │ ├── OnVideoEditedListener.kt │ └── OnVideoListener.kt │ ├── utils │ ├── BackgroundExecutor.kt │ ├── Bitmap.kt │ ├── FileUtils.kt │ ├── RealPathUtil.kt │ ├── TrimVideoUtils.kt │ ├── UiThreadExecutor.kt │ ├── VideoOptions.kt │ └── VideoQuality.kt │ └── view │ ├── ProgressBarView.kt │ ├── RangeSeekBarView.kt │ ├── Thumb.kt │ ├── TimeLineView.kt │ └── VideoEditor.kt └── res ├── drawable-hdpi ├── ic_play_video.png ├── seek_left_handle.png ├── seek_line.png ├── seek_middle_handle.png └── seek_right_handle.png ├── drawable-v21 └── play_button.xml ├── drawable ├── play_button.xml ├── rounded_textview_video_trim.xml └── seekbar_bg.xml ├── layout ├── view_cropper.xml └── view_trimmer.xml ├── values-ar └── strings.xml ├── values-es └── strings.xml ├── values-pt-rBR └── strings.xml ├── values-sw1080dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw300dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw330dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw360dp └── negative_sdps.xml ├── values-sw390dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw420dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw450dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw480dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw510dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw540dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw570dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw600dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw630dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw660dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw690dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw720dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw750dp ├── negative_sdps.xml └── positive_sdps.xml ├── values-sw780dp ├── negative_sdps.xml └── positive_sdps.xml └── values ├── attrs.xml ├── colors.xml ├── dimens.xml ├── strings.xml └── styles.xml /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/.settings 3 | **/bin 4 | **/.classpath 5 | **/.project 6 | .DS_Store 7 | 8 | # Gradle 9 | .gradle 10 | 11 | # Built application files 12 | *.apk 13 | *.ap_ 14 | 15 | # Files for the dex VM 16 | *.dex 17 | 18 | # Java class files 19 | *.class 20 | 21 | # Generated files 22 | bin/ 23 | gen/ 24 | 25 | # Local configuration file (sdk path, etc) 26 | local.properties 27 | 28 | # Windows thumbnail db 29 | Thumbs.db 30 | 31 | # OSX files 32 | .DS_Store 33 | 34 | # Eclipse project files 35 | .classpath 36 | .project 37 | 38 | # Android Studio 39 | .idea 40 | *.iml 41 | build/ 42 | 43 | /*/out 44 | /*/*/build 45 | /*/*/production 46 | *.iws 47 | *.ipr 48 | *~ 49 | *.swp 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Knowledge, education for life. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Video Trimmerw 2 | [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg?style=flat-square)](https://www.apache.org/licenses/LICENSE-2.0.html) 3 | [![](https://jitpack.io/v/mohamed0017/SimpleVideoEditor.svg)](https://jitpack.io/#mohamed0017/SimpleVideoEditor) 4 | 5 | 6 | 7 | 8 |
Video Editor
Video Editor
9 | 10 | ## About Library 11 | Simple video editor Library contains the following features (cropping/trimming/compressing) videos, using FFmpegKit Libary. 12 | 13 | ## Implementation 14 | ### [1] In your app module gradle file 15 | ```gradle 16 | dependencies { 17 | implementation 'com.github.mohamed0017:SimpleVideoEditor:' 18 | } 19 | ``` 20 | 21 | ### [2] In your project level gradle file 22 | ```gradle 23 | allprojects { 24 | repositories { 25 | maven { url 'https://jitpack.io' } 26 | } 27 | } 28 | ``` 29 | ### [3] Use VideoTrimmer in your layout.xml 30 | ```xml 31 | 36 | ``` 37 | ### [4] Implement OnVideoEditedListener on your Activity/ Fragment 38 | ```kotlin 39 | class MainActivity : AppCompatActivity(), OnTrimVideoListener { 40 | ... 41 | override fun onTrimStarted(){ 42 | } 43 | override fun getResult(uri: Uri){ 44 | } 45 | override fun cancelAction(){ 46 | } 47 | override fun onError(message: String){ 48 | } 49 | override fun onProgress(percentage: Int){ 50 | } 51 | } 52 | 53 | ``` 54 | ### [5] Create instances and set default values for the VideoTrimmer in your Activity/ Fragment 55 | ```kotlin 56 | videoTrimmer.setTextTimeSelectionTypeface(FontsHelper[this, FontsConstants.SEMI_BOLD]) 57 | .setOnTrimVideoListener(this) 58 | .setOnVideoListener(this) 59 | .setVideoURI(Uri.parse(path)) 60 | .setVideoInformationVisibility(true) 61 | .setMaxDuration(10) 62 | .setMinDuration(2) 63 | .setVideoQuality(VideoQuality.Medium) // set video quality 64 | .setDestinationPath(Environment.getExternalStorageDirectory().path + File.separator + Environment.DIRECTORY_MOVIES) 65 | ``` 66 | ### [8] Create instances and set default values for the VideoCropper in your Activity/ Fragment 67 | ```kotlin 68 | videoCropper.setVideoURI(Uri.parse(path)) 69 | .setOnCropVideoListener(this) 70 | .setMinMaxRatios(0.3f, 3f) 71 | .setDestinationPath(Environment.getExternalStorageDirectory().path + File.separator + Environment.DIRECTORY_MOVIES) 72 | ``` 73 | 74 | Voila! You have implemented an awesome Video Editor for your Android Project now! 75 | -------------------------------------------------------------------------------- /Screenshots/Screenshot_2023-08-26-16-58-44-339_com.video.sample (1).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/Screenshots/Screenshot_2023-08-26-16-58-44-339_com.video.sample (1).jpg -------------------------------------------------------------------------------- /Screenshots/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/Screenshots/linkedin.png -------------------------------------------------------------------------------- /Screenshots/ss3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/Screenshots/ss3.gif -------------------------------------------------------------------------------- /Screenshots/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/Screenshots/twitter.png -------------------------------------------------------------------------------- /Screenshots/ve1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/Screenshots/ve1.png -------------------------------------------------------------------------------- /Screenshots/ve2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/Screenshots/ve2.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.0' 3 | repositories { 4 | mavenCentral() 5 | maven { url 'https://www.jitpack.io' } 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.0.4' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | mavenCentral() 17 | maven { url 'https://www.jitpack.io' } 18 | google() 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.defaults.buildfeatures.buildconfig=true 23 | android.nonTransitiveRClass=false 24 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 22 13:40:54 IST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /sample-app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 34 6 | defaultConfig { 7 | applicationId "com.video.sample" 8 | minSdkVersion 24 9 | targetSdkVersion 34 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | buildFeatures { 21 | viewBinding = true 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_11 25 | targetCompatibility JavaVersion.VERSION_11 26 | } 27 | } 28 | 29 | repositories{ 30 | flatDir{ dirs 'libs' } 31 | maven { url 'https://jitpack.io' } 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: 'libs', include: ['*.jar']) 36 | implementation project(':video-editor') 37 | implementation 'androidx.appcompat:appcompat:1.1.0-rc01' 38 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' 39 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 41 | implementation 'com.github.tizisdeepan:PieProgress:1.0.2' 42 | } 43 | -------------------------------------------------------------------------------- /sample-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/dogo/Development/sdks/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 22 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Black.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-BlackItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-BlackItalic.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Bold.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-BoldItalic.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-ExtraBold.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-ExtraBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-ExtraBoldItalic.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-ExtraLight-BETA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-ExtraLight-BETA.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-ExtraLightItalic-BETA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-ExtraLightItalic-BETA.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Italic.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Light-BETA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Light-BETA.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-LightItalic-BETA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-LightItalic-BETA.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Medium.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-MediumItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-MediumItalic.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Regular.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-SemiBold.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-SemiBoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-SemiBoldItalic.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-Thin-BETA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-Thin-BETA.otf -------------------------------------------------------------------------------- /sample-app/src/main/assets/fonts/Inter-UI-ThinItalic-BETA.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/assets/fonts/Inter-UI-ThinItalic-BETA.otf -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/EditorActivity.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | import android.Manifest 4 | import android.content.ContentUris 5 | import android.content.ContentValues 6 | import android.content.pm.PackageManager 7 | import android.media.MediaMetadataRetriever 8 | import android.net.Uri 9 | import android.os.Bundle 10 | import android.os.Environment 11 | import android.provider.MediaStore 12 | import androidx.core.app.ActivityCompat 13 | import androidx.core.content.ContextCompat 14 | import android.util.Log 15 | import android.widget.Toast 16 | import androidx.appcompat.app.AppCompatActivity 17 | import androidx.lifecycle.lifecycleScope 18 | import com.video.sample.databinding.ActivityVideoEditorBinding 19 | import com.video.trimmer.interfaces.OnVideoEditedListener 20 | import com.video.trimmer.interfaces.OnVideoListener 21 | import com.video.trimmer.utils.fileFromContentUri 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import java.io.File 25 | 26 | class EditorActivity : AppCompatActivity(), OnVideoEditedListener, OnVideoListener { 27 | 28 | private val progressDialog: VideoProgressIndeterminateDialog by lazy { 29 | VideoProgressIndeterminateDialog( 30 | this, 31 | "Cropping video. Please wait..." 32 | ) 33 | } 34 | private lateinit var binding: ActivityVideoEditorBinding 35 | 36 | private var tempFile: File? = null 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | binding = ActivityVideoEditorBinding.inflate(layoutInflater) 40 | val view = binding.root 41 | setContentView(view) 42 | 43 | setupPermissions { 44 | val extraIntent = intent 45 | val uri: Uri? 46 | if (extraIntent != null) { 47 | uri = Uri.parse( 48 | extraIntent.getStringExtra(MainActivity.EXTRA_VIDEO_URI_STRING).toString() 49 | ) 50 | tempFile = uri?.fileFromContentUri(this) 51 | } 52 | 53 | binding.videoTrimmer.setTextTimeSelectionTypeface(FontsHelper[this, FontsConstants.SEMI_BOLD]) 54 | .setOnTrimVideoListener(this) 55 | .setOnVideoListener(this) 56 | .setVideoURI(Uri.parse(tempFile?.path)) 57 | .setBitrate(2) 58 | .setVideoInformationVisibility(true) 59 | .setMinDuration(2) 60 | .setDestinationPath(Environment.getExternalStorageDirectory().path + File.separator + Environment.DIRECTORY_MOVIES) 61 | } 62 | 63 | binding.back.setOnClickListener { 64 | binding.videoTrimmer.onCancelClicked() 65 | } 66 | 67 | 68 | binding.save.setOnClickListener { 69 | binding.videoTrimmer.onSaveClicked() 70 | } 71 | } 72 | 73 | override fun onTrimStarted() { 74 | val context = applicationContext 75 | lifecycleScope.launch(Dispatchers.Main) { 76 | Toast.makeText(context, "Started Trimming", Toast.LENGTH_SHORT).show() 77 | progressDialog.show() 78 | } 79 | } 80 | 81 | override fun getResult(uri: Uri) { 82 | val context = applicationContext 83 | lifecycleScope.launch(Dispatchers.Main) { 84 | Toast.makeText(context, "Video saved at ${uri.path}", Toast.LENGTH_SHORT).show() 85 | progressDialog.dismiss() 86 | 87 | val mediaMetadataRetriever = MediaMetadataRetriever() 88 | mediaMetadataRetriever.setDataSource(context, uri) 89 | val duration = 90 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) 91 | ?.toLong() 92 | val width = 93 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) 94 | ?.toLong() 95 | val height = 96 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) 97 | ?.toLong() 98 | val values = ContentValues() 99 | values.put(MediaStore.Video.Media.DATA, uri.path) 100 | values.put(MediaStore.Video.VideoColumns.DURATION, duration) 101 | values.put(MediaStore.Video.VideoColumns.WIDTH, width) 102 | values.put(MediaStore.Video.VideoColumns.HEIGHT, height) 103 | val id = contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values) 104 | ?.let { ContentUris.parseId(it) } 105 | Log.e("VIDEO ID", id.toString()) 106 | } 107 | 108 | } 109 | 110 | override fun cancelAction() { 111 | lifecycleScope.launch(Dispatchers.Main) { 112 | binding.videoTrimmer.destroy() 113 | // delete temp file after use it 114 | tempFile?.delete() 115 | finish() 116 | } 117 | } 118 | 119 | override fun onPause() { 120 | super.onPause() 121 | binding.videoTrimmer.onPause() 122 | } 123 | 124 | override fun onResume() { 125 | super.onResume() 126 | binding.videoTrimmer.onResume() 127 | } 128 | 129 | override fun onDestroy() { 130 | super.onDestroy() 131 | // delete temp file after use it 132 | tempFile?.delete() 133 | } 134 | 135 | override fun onError(message: String) { 136 | val context = applicationContext 137 | lifecycleScope.launch(Dispatchers.Main) { 138 | Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show() 139 | progressDialog.dismiss() 140 | } 141 | Log.e("ERROR", message) 142 | // delete temp file after use it 143 | tempFile?.delete() 144 | } 145 | 146 | override fun onProgress(percentage: Int) { 147 | lifecycleScope.launch(Dispatchers.Main) { 148 | progressDialog.updateProgress("Cropping video. Please wait.. " + 149 | "$percentage %") 150 | } 151 | } 152 | 153 | override fun onVideoPrepared() { 154 | Toast.makeText(this, "onVideoPrepared", Toast.LENGTH_SHORT).show() 155 | } 156 | 157 | lateinit var doThis: () -> Unit 158 | private fun setupPermissions(doSomething: () -> Unit) { 159 | val writePermission = 160 | ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 161 | val readPermission = 162 | ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) 163 | doThis = doSomething 164 | if (writePermission != PackageManager.PERMISSION_GRANTED && readPermission != PackageManager.PERMISSION_GRANTED) { 165 | ActivityCompat.requestPermissions( 166 | this, 167 | arrayOf( 168 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 169 | Manifest.permission.READ_EXTERNAL_STORAGE 170 | ), 171 | 101 172 | ) 173 | } else doThis() 174 | } 175 | 176 | override fun onRequestPermissionsResult( 177 | requestCode: Int, 178 | permissions: Array, 179 | grantResults: IntArray 180 | ) { 181 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 182 | when (requestCode) { 183 | 101 -> { 184 | if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { 185 | PermissionsDialog( 186 | this@EditorActivity, 187 | "To continue, give Zoho Social access to your Photos." 188 | ).show() 189 | } else doThis() 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/FontsConstants.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | object FontsConstants { 4 | const val SEMI_BOLD = "Inter-UI-Medium.otf" 5 | const val BOLD = "Inter-UI-SemiBold.otf" 6 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/FontsHelper.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | import android.content.Context 4 | import android.graphics.Typeface 5 | import android.util.Log 6 | import java.util.* 7 | 8 | object FontsHelper { 9 | private const val TAG = "TypefaceHelper" 10 | 11 | private val fontsCache = Hashtable() 12 | 13 | operator fun get(c: Context, assetPath: String): Typeface? { 14 | if (!fontsCache.containsKey(assetPath)) { 15 | try { 16 | fontsCache[assetPath] = Typeface.createFromAsset(c.assets, "fonts/$assetPath") 17 | } catch (e: Exception) { 18 | Log.e(TAG, "Could not get typeface '" + assetPath + "' because " + e.message) 19 | return null 20 | } 21 | } 22 | return fontsCache[assetPath] 23 | } 24 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.net.Uri 8 | import android.os.Bundle 9 | import android.widget.Toast 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.core.app.ActivityCompat 12 | import androidx.core.content.ContextCompat 13 | import com.video.sample.databinding.ActivityMainBinding 14 | 15 | class MainActivity : AppCompatActivity() { 16 | 17 | private lateinit var binding: ActivityMainBinding 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_main) 22 | binding = ActivityMainBinding.inflate(layoutInflater) 23 | val view = binding.root 24 | setContentView(view) 25 | binding.trimmerButton.setOnClickListener { pickFromGallery(REQUEST_VIDEO_TRIMMER) } 26 | binding.cropperButton.setOnClickListener { pickFromGallery(REQUEST_VIDEO_CROPPER) } 27 | } 28 | 29 | private fun pickFromGallery(intentCode: Int) { 30 | setupPermissions { 31 | val intent = Intent() 32 | intent.setTypeAndNormalize("video/*") 33 | intent.action = Intent.ACTION_GET_CONTENT 34 | intent.addCategory(Intent.CATEGORY_OPENABLE) 35 | startActivityForResult(Intent.createChooser(intent, getString(R.string.label_select_video)), intentCode) 36 | } 37 | } 38 | 39 | public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 40 | if (resultCode == Activity.RESULT_OK) { 41 | if (requestCode == REQUEST_VIDEO_TRIMMER) { 42 | val selectedUri = data!!.data 43 | if (selectedUri != null) { 44 | startTrimActivity(selectedUri) 45 | } else { 46 | Toast.makeText(this@MainActivity, R.string.toast_cannot_retrieve_selected_video, Toast.LENGTH_SHORT).show() 47 | } 48 | } 49 | } 50 | super.onActivityResult(requestCode, resultCode, data) 51 | } 52 | 53 | private fun startTrimActivity(uri: Uri) { 54 | val intent = Intent(this, EditorActivity::class.java) 55 | intent.putExtra(EXTRA_VIDEO_URI_STRING, uri.toString()) 56 | startActivity(intent) 57 | } 58 | 59 | companion object { 60 | private const val REQUEST_VIDEO_TRIMMER = 0x01 61 | private const val REQUEST_VIDEO_CROPPER = 0x02 62 | internal const val EXTRA_VIDEO_URI_STRING = "EXTRA_VIDEO_PATH" 63 | } 64 | 65 | lateinit var doThis: () -> Unit 66 | private fun setupPermissions(doSomething: () -> Unit) { 67 | val writePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 68 | val readPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) 69 | doThis = doSomething 70 | if (writePermission != PackageManager.PERMISSION_GRANTED && readPermission != PackageManager.PERMISSION_GRANTED) { 71 | ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), 101) 72 | } else doThis() 73 | } 74 | 75 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 76 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 77 | when (requestCode) { 78 | 101 -> { 79 | if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) { 80 | PermissionsDialog(this@MainActivity, "To continue, give Zoho Social access to your Photos.").show() 81 | } else doThis() 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/PermissionsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Color 7 | import android.graphics.drawable.ColorDrawable 8 | import android.net.Uri 9 | import android.os.Bundle 10 | import android.provider.Settings 11 | import com.video.sample.databinding.DialogPermissionsBinding 12 | 13 | class PermissionsDialog(var ctx: Context, var msg: String) : Dialog(ctx) { 14 | private lateinit var binding: DialogPermissionsBinding 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | binding = DialogPermissionsBinding.inflate(layoutInflater) 19 | val view = binding.root 20 | setContentView(view) 21 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 22 | 23 | binding.message.text = msg 24 | 25 | binding.dismiss.setOnClickListener { 26 | dismiss() 27 | } 28 | 29 | binding.settings.setOnClickListener { 30 | val i = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + BuildConfig.APPLICATION_ID)) 31 | ctx.startActivity(i) 32 | dismiss() 33 | } 34 | 35 | binding.permissionsTitle.typeface = FontsHelper[ctx, FontsConstants.BOLD] 36 | binding.message.typeface = FontsHelper[ctx, FontsConstants.SEMI_BOLD] 37 | binding.dismiss.typeface = FontsHelper[ctx, FontsConstants.SEMI_BOLD] 38 | binding.settings.typeface = FontsHelper[ctx, FontsConstants.SEMI_BOLD] 39 | } 40 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/RunOnUiThread.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/VideoProgressDialog.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.drawable.ColorDrawable 7 | import android.os.Bundle 8 | import com.video.sample.databinding.ProgressLoadingBinding 9 | 10 | class VideoProgressDialog(private var ctx: Context, private var message: String) : Dialog(ctx) { 11 | 12 | private lateinit var binding: ProgressLoadingBinding 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | binding = ProgressLoadingBinding.inflate(layoutInflater) 17 | val view = binding.root 18 | setContentView(view) 19 | 20 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 21 | setCancelable(false) 22 | setCanceledOnTouchOutside(false) 23 | 24 | binding.messageLabel.text = message 25 | 26 | binding.messageLabel.typeface = FontsHelper[ctx, FontsConstants.SEMI_BOLD] 27 | } 28 | 29 | 30 | fun setProgress(progress: Float) { 31 | binding.pieProgress.setProgress(progress) 32 | } 33 | } -------------------------------------------------------------------------------- /sample-app/src/main/java/com/video/sample/VideoProgressIndeterminateDialog.kt: -------------------------------------------------------------------------------- 1 | package com.video.sample 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.drawable.ColorDrawable 7 | import android.os.Bundle 8 | import com.video.sample.databinding.ProgressLoadingIndeterminateBinding 9 | 10 | class VideoProgressIndeterminateDialog(private var ctx: Context, private var message: String) : Dialog(ctx) { 11 | 12 | private lateinit var binding: ProgressLoadingIndeterminateBinding 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | binding = ProgressLoadingIndeterminateBinding.inflate(layoutInflater) 17 | val view = binding.root 18 | setContentView(view) 19 | 20 | window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 21 | setCancelable(false) 22 | setCanceledOnTouchOutside(false) 23 | 24 | binding.messageLabel.text = message 25 | 26 | binding.messageLabel.typeface = FontsHelper[ctx, FontsConstants.SEMI_BOLD] 27 | } 28 | 29 | fun updateProgress(newMessage : String){ 30 | binding.messageLabel.text = newMessage 31 | } 32 | } -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable-v21/background_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/back_white.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/background_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/dialog_rounded_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_crop.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_proceed_error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_trim.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/ic_videocam_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/src/main/res/drawable/textview_blue_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 23 | 24 | 25 | 31 | 32 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/activity_video_editor.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 22 | 23 | 28 | 29 | 30 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/dialog_permissions.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 23 | 24 | 33 | 34 | 35 | 36 | 40 | 41 | 48 | 49 | 56 | 57 | 68 | 69 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/progress_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /sample-app/src/main/res/layout/progress_loading_indeterminate.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 27 | 28 | -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /sample-app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #000000 6 | 7 | -------------------------------------------------------------------------------- /sample-app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 8dp 7 | 176dp 8 | 16dp 9 | 10 | 40dp 11 | 32dp 12 | 13 | 14 | 16dp 15 | 16sp 16 | 16sp 17 | 12sp 18 | 19 | 20 | 1dp 21 | 22 | 23 | 48dp 24 | 2dp 25 | 8dp 26 | 27 | 28 | 20dp 29 | 10dp 30 | 31 | 32 | @dimen/_15sdp 33 | @dimen/_14sdp 34 | @dimen/_13sdp 35 | @dimen/_12sdp 36 | @dimen/_11sdp 37 | @dimen/_10sdp 38 | @dimen/_30sdp 39 | @dimen/_9sdp 40 | 41 | -------------------------------------------------------------------------------- /sample-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VideoTrimmerSample 3 | Cancel 4 | OK 5 | Select Video 6 | Permission needed 7 | Storage read permission is needed to pick files. 8 | Cannot retrieve selected video 9 | Video saved at : $%1s 10 | A library with UI and mechanisms to trim local videos on Android applications 11 | 12 | # Provide UI to preview and select start/end of the video to trim 13 | \n\n# Thumbnails with frame by frame overview of the video 14 | \n\n# Works with gallery and recorded videos 15 | 16 | Select or record a a video below to try it out: 17 | Trimming your video… 18 | 19 | -------------------------------------------------------------------------------- /sample-app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample-app/src/main/res/xml/provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':video-editor', ':sample-app' 2 | -------------------------------------------------------------------------------- /video-editor/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'maven-publish' 4 | 5 | android { 6 | compileSdkVersion 34 7 | 8 | defaultConfig { 9 | minSdkVersion 24 10 | targetSdkVersion 34 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_11 21 | targetCompatibility JavaVersion.VERSION_11 22 | } 23 | buildFeatures { 24 | viewBinding = true 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' 31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 32 | implementation 'com.arthenica:ffmpeg-kit-min-gpl:5.1' 33 | implementation 'com.vanniktech:android-image-cropper:4.5.0' 34 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 35 | } 36 | repositories { 37 | mavenCentral() 38 | } 39 | 40 | publishing { 41 | publications { 42 | release(MavenPublication) { 43 | groupId = 'com.github.mohamed0017' 44 | artifactId = 'SimpleVideoEditor' 45 | version = '1.0.8' 46 | 47 | afterEvaluate { 48 | from components.release 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /video-editor/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/interfaces/OnProgressVideoListener.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.interfaces 2 | 3 | interface OnProgressVideoListener { 4 | fun updateProgress(time: Float, max: Float, scale: Float) 5 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/interfaces/OnRangeSeekBarListener.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.interfaces 2 | 3 | import com.video.trimmer.view.RangeSeekBarView 4 | 5 | interface OnRangeSeekBarListener { 6 | fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 7 | fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 8 | fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 9 | fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 10 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/interfaces/OnVideoEditedListener.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.interfaces 2 | 3 | import android.net.Uri 4 | 5 | interface OnVideoEditedListener { 6 | fun onTrimStarted() 7 | fun getResult(uri: Uri) 8 | fun cancelAction() 9 | fun onError(message: String) 10 | fun onProgress(percentage: Int) 11 | } 12 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/interfaces/OnVideoListener.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.interfaces 2 | 3 | interface OnVideoListener { 4 | fun onVideoPrepared() 5 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/BackgroundExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import android.util.Log 4 | import java.util.ArrayList 5 | import java.util.concurrent.* 6 | import java.util.concurrent.atomic.AtomicBoolean 7 | import kotlin.math.max 8 | 9 | object BackgroundExecutor { 10 | 11 | private const val TAG = "BackgroundExecutor" 12 | 13 | private val DEFAULT_EXECUTOR: Executor = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors()) 14 | private val executor = DEFAULT_EXECUTOR 15 | private val TASKS = ArrayList() 16 | private val CURRENT_SERIAL = ThreadLocal() 17 | 18 | /** 19 | * Execute a runnable after the given delay. 20 | * 21 | * @param runnable the task to execute 22 | * @param delay the time from now to delay execution, in milliseconds 23 | * 24 | * 25 | * if `delay` is strictly positive and the current 26 | * executor does not support scheduling (if 27 | * Executor has been called with such an 28 | * executor) 29 | * @return Future associated to the running task 30 | * @throws IllegalArgumentException if the current executor set by Executor 31 | * does not support scheduling 32 | */ 33 | private fun directExecute(runnable: Runnable, delay: Long): Future<*>? { 34 | var future: Future<*>? = null 35 | if (delay > 0) { 36 | /* no serial, but a delay: schedule the task */ 37 | if (executor !is ScheduledExecutorService) { 38 | throw IllegalArgumentException("The executor set does not support scheduling") 39 | } 40 | future = executor.schedule(runnable, delay, TimeUnit.MILLISECONDS) 41 | } else { 42 | if (executor is ExecutorService) { 43 | future = executor.submit(runnable) 44 | } else { 45 | /* non-cancellable task */ 46 | executor.execute(runnable) 47 | } 48 | } 49 | return future 50 | } 51 | 52 | /** 53 | * Execute a task after (at least) its delay **and** after all 54 | * tasks added with the same non-null `serial` (if any) have 55 | * completed execution. 56 | * 57 | * @param task the task to execute 58 | * @throws IllegalArgumentException if `task.delay` is strictly positive and the 59 | * current executor does not support scheduling (if 60 | * Executor has been called with such an 61 | * executor) 62 | */ 63 | @Synchronized 64 | fun execute(task: Task) { 65 | var future: Future<*>? = null 66 | if (task.serial == null || !hasSerialRunning(task.serial)) { 67 | task.executionAsked = true 68 | future = directExecute(task, task.remainingDelay) 69 | } 70 | if ((task.id != null || task.serial != null) && !task.managed.get()) { 71 | /* keep task */ 72 | task.future = future 73 | TASKS.add(task) 74 | } 75 | } 76 | 77 | /** 78 | * Indicates whether a task with the specified `serial` has been 79 | * submitted to the executor. 80 | * 81 | * @param serial the serial queue 82 | * @return `true` if such a task has been submitted, 83 | * `false` otherwise 84 | */ 85 | private fun hasSerialRunning(serial: String?): Boolean { 86 | for (task in TASKS) { 87 | if (task.executionAsked && serial == task.serial) { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | /** 95 | * Retrieve and remove the first task having the specified 96 | * `serial` (if any). 97 | * 98 | * @param serial the serial queue 99 | * @return task if found, `null` otherwise 100 | */ 101 | private fun take(serial: String): Task? { 102 | val len = TASKS.size 103 | for (i in 0 until len) { 104 | if (serial == TASKS[i].serial) { 105 | return TASKS.removeAt(i) 106 | } 107 | } 108 | return null 109 | } 110 | 111 | /** 112 | * Cancel all tasks having the specified `id`. 113 | * 114 | * @param id the cancellation identifier 115 | * @param mayInterruptIfRunning `true` if the thread executing this task should be 116 | * interrupted; otherwise, in-progress tasks are allowed to 117 | * complete 118 | */ 119 | @Synchronized 120 | fun cancelAll(id: String, mayInterruptIfRunning: Boolean) { 121 | for (i in TASKS.indices.reversed()) { 122 | val task = TASKS[i] 123 | if (id == task.id) { 124 | if (task.future != null) { 125 | task.future?.cancel(mayInterruptIfRunning) 126 | if (!task.managed.getAndSet(true)) { 127 | /* 128 | * the task has been submitted to the executor, but its 129 | * execution has not started yet, so that its run() 130 | * method will never call postExecute() 131 | */ 132 | task.postExecute() 133 | } 134 | } else if (task.executionAsked) { 135 | Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)") 136 | } else { 137 | /* this task has not been submitted to the executor */ 138 | TASKS.removeAt(i) 139 | } 140 | } 141 | } 142 | } 143 | 144 | abstract class Task(id: String, delay: Long, serial: String) : Runnable { 145 | 146 | var id: String? = null 147 | var remainingDelay: Long = 0 148 | private var targetTimeMillis: Long = 0 /* since epoch */ 149 | var serial: String? = null 150 | var executionAsked: Boolean = false 151 | var future: Future<*>? = null 152 | 153 | /* 154 | * A task can be cancelled after it has been submitted to the executor 155 | * but before its run() method is called. In that case, run() will never 156 | * be called, hence neither will postExecute(): the tasks with the same 157 | * serial identifier (if any) will never be submitted. 158 | * 159 | * Therefore, cancelAll() *must* call postExecute() if run() is not 160 | * started. 161 | * 162 | * This flag guarantees that either cancelAll() or run() manages this 163 | * task post execution, but not both. 164 | */ 165 | val managed = AtomicBoolean() 166 | 167 | init { 168 | if ("" != id) { 169 | this.id = id 170 | } 171 | if (delay > 0) { 172 | remainingDelay = delay 173 | targetTimeMillis = System.currentTimeMillis() + delay 174 | } 175 | if ("" != serial) { 176 | this.serial = serial 177 | } 178 | } 179 | 180 | override fun run() { 181 | if (managed.getAndSet(true)) { 182 | /* cancelled and postExecute() already called */ 183 | return 184 | } 185 | 186 | try { 187 | CURRENT_SERIAL.set(serial) 188 | execute() 189 | } finally { 190 | /* handle next tasks */ 191 | postExecute() 192 | } 193 | } 194 | 195 | abstract fun execute() 196 | 197 | fun postExecute() { 198 | if (id == null && serial == null) { 199 | /* nothing to do */ 200 | return 201 | } 202 | CURRENT_SERIAL.set(null) 203 | synchronized(BackgroundExecutor::class.java) { 204 | /* execution complete */ 205 | TASKS.remove(this) 206 | 207 | if (serial != null) { 208 | val next = take(serial!!) 209 | if (next != null) { 210 | if (next.remainingDelay != 0L) { 211 | /* the delay may not have elapsed yet */ 212 | next.remainingDelay = max(0L, targetTimeMillis - System.currentTimeMillis()) 213 | } 214 | /* a task having the same serial was queued, execute it */ 215 | execute(next) 216 | } 217 | } 218 | } 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.drawable.BitmapDrawable 6 | import android.graphics.drawable.Drawable 7 | 8 | fun drawableToBitmap(drawable: Drawable, width: Int, height: Int): Bitmap? { 9 | if (drawable is BitmapDrawable) { 10 | return drawable.bitmap 11 | } 12 | val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 13 | val canvas = Canvas(bitmap) 14 | drawable.setBounds(0, 0, 0, 0) 15 | drawable.draw(canvas) 16 | return bitmap 17 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.webkit.MimeTypeMap 6 | import java.io.File 7 | import java.io.FileOutputStream 8 | import java.io.IOException 9 | import java.io.InputStream 10 | import java.io.OutputStream 11 | 12 | fun Uri.fileFromContentUri(context: Context): File { 13 | // Preparing Temp file name 14 | val fileExtension = getFileExtension(context, this) 15 | val fileName = "temp_file" + if (fileExtension != null) ".$fileExtension" else "" 16 | 17 | // Creating Temp file 18 | val tempFile = File(context.cacheDir, fileName) 19 | tempFile.createNewFile() 20 | 21 | try { 22 | val oStream = FileOutputStream(tempFile) 23 | val inputStream = context.contentResolver.openInputStream(this) 24 | 25 | inputStream?.let { 26 | copy(inputStream, oStream) 27 | } 28 | 29 | oStream.flush() 30 | } catch (e: Exception) { 31 | e.printStackTrace() 32 | } 33 | 34 | return tempFile 35 | } 36 | 37 | private fun getFileExtension(context: Context, uri: Uri): String? { 38 | val fileType: String? = context.contentResolver.getType(uri) 39 | return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType) 40 | } 41 | 42 | @Throws(IOException::class) 43 | private fun copy(source: InputStream, target: OutputStream) { 44 | val buf = ByteArray(8192) 45 | var length: Int 46 | while (source.read(buf).also { length = it } > 0) { 47 | target.write(buf, 0, length) 48 | } 49 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/RealPathUtil.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ContentUris 5 | import android.content.Context 6 | import android.net.Uri 7 | import android.os.Environment 8 | import android.provider.DocumentsContract 9 | import android.provider.MediaStore 10 | 11 | object RealPathUtil { 12 | 13 | @SuppressLint("NewApi") 14 | fun realPathFromUriApi19(context: Context, uri: Uri): String? { 15 | if (DocumentsContract.isDocumentUri(context, uri)) { 16 | if (isExternalStorageDocument(uri)) { 17 | val docId = DocumentsContract.getDocumentId(uri) 18 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 19 | val type = split[0] 20 | if ("primary".equals(type, ignoreCase = true)) return Environment.getExternalStorageDirectory().toString() + "/" + split[1] 21 | } else if (isDownloadsDocument(uri)) { 22 | val id = DocumentsContract.getDocumentId(uri) 23 | val contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) 24 | return getDataColumn(context, contentUri, null, null) 25 | } else if (isMediaDocument(uri)) { 26 | val docId = DocumentsContract.getDocumentId(uri) 27 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 28 | val type = split[0] 29 | var contentUri: Uri? = null 30 | when (type) { 31 | "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 32 | "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI 33 | "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 34 | } 35 | val selection = "_id=?" 36 | val selectionArgs = arrayOf(split[1]) 37 | return getDataColumn(context, contentUri, selection, selectionArgs) 38 | } 39 | } else if ("content".equals(uri.scheme, ignoreCase = true)) { 40 | return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) 41 | } else if ("file".equals(uri.scheme, ignoreCase = true)) { 42 | return uri.path 43 | } 44 | return null 45 | } 46 | 47 | private fun isGooglePhotosUri(uri: Uri): Boolean = "com.google.android.apps.photos.content" == uri.authority 48 | 49 | @SuppressLint("Recycle") 50 | private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { 51 | val column = "_data" 52 | val projection = arrayOf(column) 53 | val cursor = if (uri != null) context.contentResolver.query(uri, projection, selection, selectionArgs, null) else null 54 | cursor.use { 55 | if (it != null && it.moveToFirst()) { 56 | val index = it.getColumnIndexOrThrow(column) 57 | val result = it.getString(index) 58 | it.close() 59 | return result 60 | } 61 | } 62 | return null 63 | } 64 | 65 | private fun isExternalStorageDocument(uri: Uri): Boolean = "com.android.externalstorage.documents" == uri.authority 66 | 67 | private fun isDownloadsDocument(uri: Uri): Boolean = "com.android.providers.downloads.documents" == uri.authority 68 | 69 | private fun isMediaDocument(uri: Uri): Boolean = "com.android.providers.media.documents" == uri.authority 70 | 71 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/TrimVideoUtils.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import java.util.* 4 | 5 | object TrimVideoUtils { 6 | 7 | fun stringForTime(timeMs: Float): String { 8 | val totalSeconds = (timeMs / 1000).toInt() 9 | val seconds = totalSeconds % 60 10 | val minutes = totalSeconds / 60 % 60 11 | val hours = totalSeconds / 3600 12 | val mFormatter = Formatter(Locale.ENGLISH) 13 | return if (hours > 0) { 14 | mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() 15 | } else { 16 | mFormatter.format("%02d:%02d", minutes, seconds).toString() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/UiThreadExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.os.Message 6 | import android.os.SystemClock 7 | import java.util.HashMap 8 | 9 | object UiThreadExecutor { 10 | 11 | private val HANDLER = object : Handler(Looper.getMainLooper()) { 12 | override fun handleMessage(msg: Message) { 13 | val callback = msg.callback 14 | if (callback != null) { 15 | callback.run() 16 | decrementToken(msg.obj as Token) 17 | } else { 18 | super.handleMessage(msg) 19 | } 20 | } 21 | } 22 | 23 | private val TOKENS = HashMap() 24 | 25 | fun runTask(id: String, task: Runnable, delay: Long) { 26 | if ("" == id) { 27 | HANDLER.postDelayed(task, delay) 28 | return 29 | } 30 | val time = SystemClock.uptimeMillis() + delay 31 | HANDLER.postAtTime(task, nextToken(id), time) 32 | } 33 | 34 | private fun nextToken(id: String): Token { 35 | synchronized(TOKENS) { 36 | var token: Token? = TOKENS[id] 37 | if (token == null) { 38 | token = Token(id) 39 | TOKENS[id] = token 40 | } 41 | token.runnablesCount++ 42 | return token 43 | } 44 | } 45 | 46 | private fun decrementToken(token: Token) { 47 | synchronized(TOKENS) { 48 | if (--token.runnablesCount == 0) { 49 | val id = token.id 50 | val old = TOKENS.remove(id) 51 | if (old != token) { 52 | // a runnable finished after cancelling, we just removed a 53 | // wrong token, lets put it back 54 | if (old != null) TOKENS[id] = old 55 | } 56 | } 57 | } 58 | } 59 | 60 | fun cancelAll(id: String) { 61 | val token: Token? 62 | synchronized(TOKENS) { 63 | token = TOKENS.remove(id) 64 | } 65 | if (token == null) { 66 | // nothing to cancel 67 | return 68 | } 69 | HANDLER.removeCallbacksAndMessages(token) 70 | } 71 | 72 | private class Token(internal val id: String) { 73 | internal var runnablesCount = 0 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/VideoOptions.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.util.Log 6 | import com.arthenica.ffmpegkit.FFmpegKit 7 | import com.arthenica.ffmpegkit.FFmpegKitConfig 8 | 9 | import com.arthenica.ffmpegkit.ReturnCode 10 | import com.video.trimmer.interfaces.OnVideoEditedListener 11 | 12 | 13 | class VideoOptions(private var ctx: Context) { 14 | companion object { 15 | const val TAG = "VideoOptions" 16 | } 17 | 18 | fun trimAndCropVideo( 19 | width: Int, 20 | height: Int, 21 | x: Int, 22 | y: Int, 23 | bitrate: Double, 24 | totalVideoDuration: Long, 25 | startPosition: String, 26 | endPosition: String, 27 | inputPath: String, 28 | outputPath: String, 29 | outputFileUri: Uri, 30 | listener: OnVideoEditedListener? 31 | ) { 32 | val command = StringBuilder() 33 | .append("-y") //overWrite 34 | .append(" -i ").append(inputPath) 35 | .append(" -ss ").append(startPosition) 36 | .append(" -to ").append(endPosition) 37 | .append(" -filter:v ") 38 | .append(" crop=").append("$width:$height:$x:$y") 39 | .append(" -vcodec libx264 -b ${bitrate}M -preset ultrafast ") 40 | .append(" -c:a copy ").append(outputPath) 41 | 42 | FFmpegKitConfig.enableStatisticsCallback { newStatistics -> 43 | val timeInMilliseconds = newStatistics.time; 44 | if (timeInMilliseconds > 0) { 45 | val completePercentage = (timeInMilliseconds * 100) / totalVideoDuration 46 | listener?.onProgress(completePercentage.toInt()) 47 | } 48 | } 49 | 50 | val session = FFmpegKit.execute(command.toString()) 51 | if (ReturnCode.isSuccess(session.returnCode)) { 52 | // SUCCESS2 53 | listener?.getResult(outputFileUri) 54 | Log.e(TAG, "onFinish: ") 55 | } else if (ReturnCode.isCancel(session.returnCode)) { 56 | // CANCEL 57 | listener?.onError("CANCEL") 58 | Log.e(TAG, "isCancel: " + "CANCEL") 59 | } else { 60 | // FAILURE 61 | listener?.onError("Failed") 62 | Log.e(TAG, "onFailure: " + "Failed") 63 | } 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/utils/VideoQuality.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.utils 2 | 3 | enum class VideoQuality { 4 | High, Medium, Low , VeryHigh 5 | } -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/view/ProgressBarView.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.view 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.Rect 7 | import android.util.AttributeSet 8 | import android.view.View 9 | import com.video.trimmer.R 10 | import com.video.trimmer.interfaces.OnProgressVideoListener 11 | import com.video.trimmer.interfaces.OnRangeSeekBarListener 12 | 13 | class ProgressBarView @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr), OnRangeSeekBarListener, OnProgressVideoListener { 14 | 15 | private var mProgressHeight: Int = 0 16 | private var mViewWidth: Int = 0 17 | 18 | private val mBackgroundColor = Paint() 19 | private val mProgressColor = Paint() 20 | 21 | private var mBackgroundRect: Rect? = null 22 | private var mProgressRect: Rect? = null 23 | 24 | init { 25 | init() 26 | } 27 | 28 | private fun init() { 29 | val lineProgress = context.getColor( R.color.progress_color) 30 | val lineBackground = context.getColor(R.color.background_progress_color) 31 | 32 | mProgressHeight = context.resources.getDimensionPixelOffset(R.dimen.progress_video_line_height) 33 | 34 | mBackgroundColor.isAntiAlias = true 35 | mBackgroundColor.color = lineBackground 36 | 37 | mProgressColor.isAntiAlias = true 38 | mProgressColor.color = lineProgress 39 | } 40 | 41 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 42 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 43 | 44 | val minW = paddingLeft + paddingRight + suggestedMinimumWidth 45 | mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1) 46 | 47 | val minH = paddingBottom + paddingTop + mProgressHeight 48 | val viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1) 49 | 50 | setMeasuredDimension(mViewWidth, viewHeight) 51 | } 52 | 53 | override fun onDraw(canvas: Canvas) { 54 | super.onDraw(canvas) 55 | 56 | drawLineBackground(canvas) 57 | drawLineProgress(canvas) 58 | } 59 | 60 | private fun drawLineBackground(canvas: Canvas) { 61 | if (mBackgroundRect != null) { 62 | canvas.drawRect(mBackgroundRect!!, mBackgroundColor) 63 | } 64 | } 65 | 66 | private fun drawLineProgress(canvas: Canvas) { 67 | if (mProgressRect != null) { 68 | canvas.drawRect(mProgressRect!!, mProgressColor) 69 | } 70 | } 71 | 72 | override fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 73 | updateBackgroundRect(index, value) 74 | } 75 | 76 | override fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 77 | updateBackgroundRect(index, value) 78 | } 79 | 80 | override fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 81 | updateBackgroundRect(index, value) 82 | } 83 | 84 | override fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 85 | updateBackgroundRect(index, value) 86 | } 87 | 88 | private fun updateBackgroundRect(index: Int, value: Float) { 89 | if (mBackgroundRect == null) mBackgroundRect = Rect(0, 0, mViewWidth, mProgressHeight) 90 | val newValue = (mViewWidth * value / 100).toInt() 91 | mBackgroundRect = if (index == 0) { 92 | Rect(newValue, mBackgroundRect!!.top, mBackgroundRect!!.right, mBackgroundRect!!.bottom) 93 | } else { 94 | Rect(mBackgroundRect!!.left, mBackgroundRect!!.top, newValue, mBackgroundRect!!.bottom) 95 | } 96 | updateProgress(0f, 0f, 0.0f) 97 | } 98 | 99 | override fun updateProgress(time: Float, max: Float, scale: Float) { 100 | if (mBackgroundRect != null) { 101 | mProgressRect = if (scale == 0f) { 102 | Rect(0, mBackgroundRect!!.top, 0, mBackgroundRect!!.bottom) 103 | } else { 104 | val newValue = (mViewWidth * scale / 100).toInt() 105 | Rect(mBackgroundRect!!.left, mBackgroundRect!!.top, newValue, mBackgroundRect!!.bottom) 106 | } 107 | } 108 | invalidate() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/view/RangeSeekBarView.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.view 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Canvas 6 | import android.graphics.Paint 7 | import android.graphics.Rect 8 | import android.util.AttributeSet 9 | import android.view.MotionEvent 10 | import android.view.View 11 | import com.video.trimmer.R 12 | import com.video.trimmer.interfaces.OnRangeSeekBarListener 13 | 14 | 15 | class RangeSeekBarView @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { 16 | 17 | private var mHeightTimeLine = 0 18 | lateinit var thumbs: List 19 | private var mListeners: MutableList? = null 20 | private var mMaxWidth = 0f 21 | private var mThumbWidth = 0f 22 | private var mThumbHeight = 0f 23 | private var mViewWidth = 0 24 | private var mPixelRangeMin = 0f 25 | private var mPixelRangeMax = 0f 26 | private var mScaleRangeMax = 0f 27 | private var mFirstRun = false 28 | 29 | private val mShadow = Paint() 30 | private val mLine = Paint() 31 | 32 | private var currentThumb = 0 33 | 34 | init { 35 | init() 36 | } 37 | 38 | private fun init() { 39 | thumbs = Thumb.initThumbs(resources) 40 | mThumbWidth = Thumb.getWidthBitmap(thumbs).toFloat() 41 | mThumbHeight = Thumb.getHeightBitmap(thumbs).toFloat() 42 | 43 | mScaleRangeMax = 100f 44 | mHeightTimeLine = context.resources.getDimensionPixelOffset(R.dimen.frames_video_height) 45 | 46 | isFocusable = true 47 | isFocusableInTouchMode = true 48 | 49 | mFirstRun = true 50 | 51 | val shadowColor = context.getColor( R.color.shadow_color) 52 | mShadow.isAntiAlias = true 53 | mShadow.color = shadowColor 54 | mShadow.alpha = 177 55 | 56 | val lineColor = context.getColor( R.color.line_color) 57 | mLine.isAntiAlias = true 58 | mLine.color = lineColor 59 | mLine.alpha = 200 60 | } 61 | 62 | fun initMaxWidth() { 63 | mMaxWidth = thumbs[1].pos - thumbs[0].pos 64 | onSeekStop(this, 0, thumbs[0].value) 65 | onSeekStop(this, 1, thumbs[1].value) 66 | } 67 | 68 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 69 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 70 | 71 | val minW = paddingLeft + paddingRight + suggestedMinimumWidth 72 | mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1) 73 | 74 | val minH = paddingBottom + paddingTop + mThumbHeight.toInt() + mHeightTimeLine 75 | val viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1) 76 | 77 | setMeasuredDimension(mViewWidth, viewHeight) 78 | 79 | mPixelRangeMin = 0f 80 | mPixelRangeMax = mViewWidth - mThumbWidth 81 | 82 | if (mFirstRun) { 83 | for (i in thumbs.indices) { 84 | val th = thumbs[i] 85 | th.value = mScaleRangeMax * i 86 | th.pos = mPixelRangeMax * i 87 | } 88 | onCreate(this, currentThumb, getThumbValue(currentThumb)) 89 | mFirstRun = false 90 | } 91 | } 92 | 93 | override fun onDraw(canvas: Canvas) { 94 | super.onDraw(canvas) 95 | drawShadow(canvas) 96 | drawThumbs(canvas) 97 | } 98 | 99 | @SuppressLint("ClickableViewAccessibility") 100 | override fun onTouchEvent(ev: MotionEvent): Boolean { 101 | val mThumb: Thumb 102 | val mThumb2: Thumb 103 | val coordinate = ev.x 104 | when (ev.action) { 105 | MotionEvent.ACTION_DOWN -> { 106 | currentThumb = getClosestThumb(coordinate) 107 | if (currentThumb == -1) return false 108 | mThumb = thumbs[currentThumb] 109 | mThumb.lastTouchX = coordinate 110 | onSeekStart(this, currentThumb, mThumb.value) 111 | return true 112 | } 113 | MotionEvent.ACTION_UP -> { 114 | if (currentThumb == -1) return false 115 | mThumb = thumbs[currentThumb] 116 | onSeekStop(this, currentThumb, mThumb.value) 117 | return true 118 | } 119 | 120 | MotionEvent.ACTION_MOVE -> { 121 | mThumb = thumbs[currentThumb] 122 | mThumb2 = thumbs[if (currentThumb == 0) 1 else 0] 123 | // Calculate the distance moved 124 | val dx = coordinate - mThumb.lastTouchX 125 | val newX = mThumb.pos + dx 126 | if (currentThumb == 0) { 127 | when { 128 | newX + mThumb.widthBitmap >= mThumb2.pos -> mThumb.pos = mThumb2.pos - mThumb.widthBitmap 129 | newX <= mPixelRangeMin -> mThumb.pos = mPixelRangeMin 130 | else -> { 131 | checkPositionThumb(mThumb, mThumb2, dx, true) 132 | mThumb.pos = mThumb.pos + dx 133 | mThumb.lastTouchX = coordinate 134 | } 135 | } 136 | 137 | } else { 138 | when { 139 | newX <= mThumb2.pos + mThumb2.widthBitmap -> mThumb.pos = mThumb2.pos + mThumb.widthBitmap 140 | newX >= mPixelRangeMax -> mThumb.pos = mPixelRangeMax 141 | else -> { 142 | checkPositionThumb(mThumb2, mThumb, dx, false) 143 | mThumb.pos = mThumb.pos + dx 144 | mThumb.lastTouchX = coordinate 145 | } 146 | } 147 | } 148 | 149 | setThumbPos(currentThumb, mThumb.pos) 150 | invalidate() 151 | return true 152 | } 153 | } 154 | return false 155 | } 156 | 157 | private fun checkPositionThumb(mThumbLeft: Thumb, mThumbRight: Thumb, dx: Float, isLeftMove: Boolean) { 158 | if (isLeftMove && dx < 0) { 159 | if (mThumbRight.pos + dx - mThumbLeft.pos > mMaxWidth) { 160 | mThumbRight.pos = mThumbLeft.pos + dx + mMaxWidth 161 | setThumbPos(1, mThumbRight.pos) 162 | } 163 | } else if (!isLeftMove && dx > 0) { 164 | if (mThumbRight.pos + dx - mThumbLeft.pos > mMaxWidth) { 165 | mThumbLeft.pos = mThumbRight.pos + dx - mMaxWidth 166 | setThumbPos(0, mThumbLeft.pos) 167 | } 168 | } 169 | } 170 | 171 | private fun getUnstuckFrom(index: Int): Int { 172 | val unstuck = 0 173 | val lastVal = thumbs[index].value 174 | for (i in index - 1 downTo 0) { 175 | val th = thumbs[i] 176 | if (th.value != lastVal) 177 | return i + 1 178 | } 179 | return unstuck 180 | } 181 | 182 | private fun pixelToScale(index: Int, pixelValue: Float): Float { 183 | val scale = pixelValue * 100 / mPixelRangeMax 184 | return if (index == 0) { 185 | val pxThumb = scale * mThumbWidth / 100 186 | scale + pxThumb * 100 / mPixelRangeMax 187 | } else { 188 | val pxThumb = (100 - scale) * mThumbWidth / 100 189 | scale - pxThumb * 100 / mPixelRangeMax 190 | } 191 | } 192 | 193 | private fun scaleToPixel(index: Int, scaleValue: Float): Float { 194 | val px = scaleValue * mPixelRangeMax / 100 195 | return if (index == 0) { 196 | val pxThumb = scaleValue * mThumbWidth / 100 197 | px - pxThumb 198 | } else { 199 | val pxThumb = (100 - scaleValue) * mThumbWidth / 100 200 | px + pxThumb 201 | } 202 | } 203 | 204 | private fun calculateThumbValue(index: Int) { 205 | if (index < thumbs.size && thumbs.isNotEmpty()) { 206 | val th = thumbs[index] 207 | th.value = pixelToScale(index, th.pos) 208 | onSeek(this, index, th.value) 209 | } 210 | } 211 | 212 | private fun calculateThumbPos(index: Int) { 213 | if (index < thumbs.size && thumbs.isNotEmpty()) { 214 | val th = thumbs[index] 215 | th.pos = scaleToPixel(index, th.value) 216 | } 217 | } 218 | 219 | private fun getThumbValue(index: Int): Float = thumbs[index].value 220 | 221 | fun setThumbValue(index: Int, value: Float) { 222 | thumbs[index].value = value 223 | calculateThumbPos(index) 224 | invalidate() 225 | } 226 | 227 | private fun setThumbPos(index: Int, pos: Float) { 228 | thumbs[index].pos = pos 229 | calculateThumbValue(index) 230 | invalidate() 231 | } 232 | 233 | private fun getClosestThumb(coordinate: Float): Int { 234 | var closest = -1 235 | if (thumbs.isNotEmpty()) { 236 | for (i in thumbs.indices) { 237 | val tcoordinate = thumbs[i].pos + mThumbWidth 238 | if (coordinate >= thumbs[i].pos && coordinate <= tcoordinate) { 239 | closest = thumbs[i].index 240 | } 241 | } 242 | } 243 | return closest 244 | } 245 | 246 | private fun drawShadow(canvas: Canvas) { 247 | if (thumbs.isNotEmpty()) { 248 | for (th in thumbs) { 249 | if (th.index == 0) { 250 | val x = th.pos + paddingLeft 251 | if (x > mPixelRangeMin) { 252 | val mRect = Rect(mThumbWidth.toInt(), 0, (x + mThumbWidth).toInt(), mHeightTimeLine) 253 | canvas.drawRect(mRect, mShadow) 254 | } 255 | } else { 256 | val x = th.pos - paddingRight 257 | if (x < mPixelRangeMax) { 258 | val mRect = Rect(x.toInt(), 0, (mViewWidth - mThumbWidth).toInt(), mHeightTimeLine) 259 | canvas.drawRect(mRect, mShadow) 260 | } 261 | } 262 | } 263 | } 264 | } 265 | 266 | private fun drawThumbs(canvas: Canvas) { 267 | if (thumbs.isNotEmpty()) { 268 | for (th in thumbs) { 269 | if (th.index == 0) { 270 | if (th.bitmap != null) canvas.drawBitmap(th.bitmap!!, th.pos + paddingLeft, 0f, null) 271 | } else { 272 | if (th.bitmap != null) canvas.drawBitmap(th.bitmap!!, th.pos - paddingRight, 0f, null) 273 | } 274 | } 275 | } 276 | } 277 | 278 | fun addOnRangeSeekBarListener(listener: OnRangeSeekBarListener) { 279 | if (mListeners == null) mListeners = ArrayList() 280 | mListeners?.add(listener) 281 | } 282 | 283 | private fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 284 | if (mListeners == null) return 285 | else { 286 | for (item in mListeners!!) { 287 | item.onCreate(rangeSeekBarView, index, value) 288 | } 289 | } 290 | } 291 | 292 | private fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 293 | if (mListeners == null) return 294 | else { 295 | for (item in mListeners!!) { 296 | item.onSeek(rangeSeekBarView, index, value) 297 | } 298 | } 299 | } 300 | 301 | private fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 302 | if (mListeners == null) return 303 | else { 304 | for (item in mListeners!!) { 305 | item.onSeekStart(rangeSeekBarView, index, value) 306 | } 307 | } 308 | } 309 | 310 | private fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 311 | if (mListeners == null) return 312 | else { 313 | for (item in mListeners!!) { 314 | item.onSeekStop(rangeSeekBarView, index, value) 315 | } 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/view/Thumb.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.view 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.Canvas 7 | import android.graphics.drawable.BitmapDrawable 8 | import android.graphics.drawable.Drawable 9 | import com.video.trimmer.R 10 | import java.util.* 11 | 12 | class Thumb private constructor() { 13 | 14 | var index: Int = 0 15 | private set 16 | var value: Float = 0.toFloat() 17 | var pos: Float = 0.toFloat() 18 | var bitmap: Bitmap? = null 19 | private set(bitmap) { 20 | field = bitmap 21 | widthBitmap = bitmap?.width ?: 0 22 | heightBitmap = bitmap?.height ?: 0 23 | } 24 | var widthBitmap: Int = 0 25 | private set 26 | private var heightBitmap: Int = 0 27 | 28 | var lastTouchX: Float = 0.toFloat() 29 | 30 | init { 31 | value = 0f 32 | pos = 0f 33 | } 34 | 35 | companion object { 36 | const val LEFT = 0 37 | const val RIGHT = 1 38 | 39 | fun initThumbs(resources: Resources): List { 40 | val thumbs = Vector() 41 | for (i in 0..1) { 42 | val th = Thumb() 43 | th.index = i 44 | if (i == 0) { 45 | val resImageLeft = R.drawable.seek_left_handle 46 | th.bitmap = BitmapFactory.decodeResource(resources, resImageLeft) 47 | // th.bitmap = drawableToBitmap(resources.getDrawable(R.drawable.seek_left_handle)) 48 | } else { 49 | val resImageRight = R.drawable.seek_right_handle 50 | th.bitmap = BitmapFactory.decodeResource(resources, resImageRight) 51 | // th.bitmap = drawableToBitmap(resources.getDrawable(R.drawable.seek_right_handle)) 52 | } 53 | thumbs.add(th) 54 | } 55 | return thumbs 56 | } 57 | 58 | fun getWidthBitmap(thumbs: List): Int = thumbs[0].widthBitmap 59 | 60 | fun getHeightBitmap(thumbs: List): Int = thumbs[0].heightBitmap 61 | 62 | fun drawableToBitmap(drawable: Drawable): Bitmap { 63 | if (drawable is BitmapDrawable && drawable.bitmap != null) return drawable.bitmap 64 | val bitmap = if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) 65 | else Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888) 66 | if (bitmap != null) { 67 | val canvas = Canvas(bitmap) 68 | drawable.setBounds(0, 0, canvas.width, canvas.height) 69 | drawable.draw(canvas) 70 | } 71 | return bitmap 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/view/TimeLineView.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.view 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.media.MediaMetadataRetriever 7 | import android.net.Uri 8 | import android.util.AttributeSet 9 | import android.util.LongSparseArray 10 | import android.view.View 11 | import com.video.trimmer.R 12 | import com.video.trimmer.utils.BackgroundExecutor 13 | import com.video.trimmer.utils.UiThreadExecutor 14 | import kotlin.math.ceil 15 | 16 | class TimeLineView @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { 17 | 18 | private var mVideoUri: Uri? = null 19 | private var mHeightView: Int = 0 20 | private var mBitmapList: LongSparseArray? = null 21 | 22 | init { 23 | init() 24 | } 25 | 26 | private fun init() { 27 | mHeightView = context.resources.getDimensionPixelOffset(R.dimen.frames_video_height) 28 | } 29 | 30 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 31 | val minW = paddingLeft + paddingRight + suggestedMinimumWidth 32 | val w = resolveSizeAndState(minW, widthMeasureSpec, 1) 33 | val minH = paddingBottom + paddingTop + mHeightView 34 | val h = resolveSizeAndState(minH, heightMeasureSpec, 1) 35 | setMeasuredDimension(w, h) 36 | } 37 | 38 | override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) { 39 | super.onSizeChanged(w, h, oldW, oldH) 40 | if (w != oldW) getBitmap(w) 41 | } 42 | 43 | private fun getBitmap(viewWidth: Int) { 44 | BackgroundExecutor.execute(object : BackgroundExecutor.Task("", 0L, "") { 45 | override fun execute() { 46 | try { 47 | val threshold = 11 48 | val thumbnailList = LongSparseArray() 49 | val mediaMetadataRetriever = MediaMetadataRetriever() 50 | mediaMetadataRetriever.setDataSource(context, mVideoUri) 51 | val videoLengthInMs = (Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000).toLong() 52 | val frameHeight = mHeightView 53 | val initialBitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) 54 | val frameWidth = ((initialBitmap?.width?.toFloat()!! / initialBitmap?.height?.toFloat()!!) * frameHeight.toFloat()).toInt() 55 | var numThumbs = ceil((viewWidth.toFloat() / frameWidth)).toInt() 56 | if (numThumbs < threshold) numThumbs = threshold 57 | val cropWidth = viewWidth / threshold 58 | val interval = videoLengthInMs / numThumbs 59 | for (i in 0 until numThumbs) { 60 | var bitmap = mediaMetadataRetriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) 61 | if (bitmap != null) { 62 | try { 63 | bitmap = Bitmap.createScaledBitmap(bitmap, frameWidth, frameHeight, false) 64 | bitmap = Bitmap.createBitmap(bitmap, 0, 0, cropWidth, bitmap.height) 65 | } catch (e: Exception) { 66 | e.printStackTrace() 67 | } 68 | thumbnailList.put(i.toLong(), bitmap) 69 | } 70 | } 71 | mediaMetadataRetriever.release() 72 | returnBitmaps(thumbnailList) 73 | } catch (e: Throwable) { 74 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e) 75 | } 76 | } 77 | }) 78 | } 79 | 80 | private fun returnBitmaps(thumbnailList: LongSparseArray) { 81 | UiThreadExecutor.runTask("", Runnable { 82 | mBitmapList = thumbnailList 83 | invalidate() 84 | }, 0L) 85 | } 86 | 87 | override fun onDraw(canvas: Canvas) { 88 | super.onDraw(canvas) 89 | if (mBitmapList != null) { 90 | canvas.save() 91 | var x = 0 92 | for (i in 0 until (mBitmapList?.size() ?: 0)) { 93 | val bitmap = mBitmapList?.get(i.toLong()) 94 | if (bitmap != null) { 95 | canvas.drawBitmap(bitmap, x.toFloat(), 0f, null) 96 | x += bitmap.width 97 | } 98 | } 99 | } 100 | } 101 | 102 | fun setVideo(data: Uri) { 103 | mVideoUri = data 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /video-editor/src/main/java/com/video/trimmer/view/VideoEditor.kt: -------------------------------------------------------------------------------- 1 | package com.video.trimmer.view 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Typeface 6 | import android.media.MediaExtractor 7 | import android.media.MediaFormat 8 | import android.media.MediaMetadataRetriever 9 | import android.media.MediaPlayer 10 | import android.net.Uri 11 | import android.os.Environment 12 | import android.os.Handler 13 | import android.os.Message 14 | import android.util.AttributeSet 15 | import android.util.Log 16 | import android.view.GestureDetector 17 | import android.view.LayoutInflater 18 | import android.view.MotionEvent 19 | import android.view.View 20 | import android.widget.FrameLayout 21 | import android.widget.SeekBar 22 | import androidx.lifecycle.findViewTreeLifecycleOwner 23 | import androidx.lifecycle.lifecycleScope 24 | import com.video.trimmer.R 25 | import com.video.trimmer.databinding.ViewTrimmerBinding 26 | import com.video.trimmer.interfaces.OnProgressVideoListener 27 | import com.video.trimmer.interfaces.OnRangeSeekBarListener 28 | import com.video.trimmer.interfaces.OnVideoEditedListener 29 | import com.video.trimmer.interfaces.OnVideoListener 30 | import com.video.trimmer.utils.BackgroundExecutor 31 | import com.video.trimmer.utils.RealPathUtil 32 | import com.video.trimmer.utils.TrimVideoUtils 33 | import com.video.trimmer.utils.UiThreadExecutor 34 | import com.video.trimmer.utils.VideoOptions 35 | import com.video.trimmer.utils.VideoQuality 36 | import com.video.trimmer.utils.drawableToBitmap 37 | import kotlinx.coroutines.Dispatchers 38 | import kotlinx.coroutines.launch 39 | import java.io.File 40 | import java.lang.ref.WeakReference 41 | import java.util.Calendar 42 | import java.util.Locale 43 | import kotlin.math.abs 44 | 45 | class VideoEditor @JvmOverloads constructor( 46 | context: Context, 47 | attrs: AttributeSet, 48 | defStyleAttr: Int = 0 49 | ) : FrameLayout(context, attrs, defStyleAttr) { 50 | 51 | private lateinit var mSrc: Uri 52 | private var mFinalPath: String? = null 53 | 54 | private var mMaxDuration: Int = -1 55 | private var mMinDuration: Int = -1 56 | private var mListeners: ArrayList = ArrayList() 57 | 58 | private var mOnVideoEditedListener: OnVideoEditedListener? = null 59 | private var mOnVideoListener: OnVideoListener? = null 60 | 61 | private lateinit var binding: ViewTrimmerBinding 62 | 63 | private var mDuration = 0f 64 | private var mTimeVideo = 0f 65 | private var mStartPosition = 0f 66 | 67 | private var mEndPosition = 0f 68 | private var mResetSeekBar = true 69 | private val mMessageHandler = MessageHandler(this) 70 | private var originalVideoWidth: Int = 0 71 | private var originalVideoHeight: Int = 0 72 | private var videoPlayerWidth: Int = 0 73 | private var videoPlayerHeight: Int = 0 74 | private var bitRate: Int = 2 75 | private var isVideoPrepared = false 76 | private var videoPlayerCurrentPosition = 0 77 | private var videoQuality = VideoQuality.Medium 78 | private var destinationPath: String 79 | get() { 80 | if (mFinalPath == null) { 81 | val folder = Environment.getExternalStorageDirectory() 82 | mFinalPath = folder.path + File.separator 83 | } 84 | return mFinalPath ?: "" 85 | } 86 | set(finalPath) { 87 | mFinalPath = finalPath 88 | } 89 | 90 | init { 91 | init(context) 92 | } 93 | 94 | private fun init(context: Context) { 95 | binding = ViewTrimmerBinding.inflate(LayoutInflater.from(context), this, true) 96 | setUpListeners() 97 | setUpMargins() 98 | } 99 | 100 | @SuppressLint("ClickableViewAccessibility") 101 | private fun setUpListeners() { 102 | mListeners = ArrayList() 103 | mListeners.add(object : OnProgressVideoListener { 104 | override fun updateProgress(time: Float, max: Float, scale: Float) { 105 | updateVideoProgress(time) 106 | } 107 | }) 108 | 109 | val gestureDetector = 110 | GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { 111 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean { 112 | onClickVideoPlayPause() 113 | return true 114 | } 115 | }) 116 | 117 | binding.iconVideoPlay.setOnClickListener { 118 | onClickVideoPlayPause() 119 | } 120 | binding.videoLoader.setOnErrorListener { _, what, _ -> 121 | mOnVideoEditedListener?.onError("Something went wrong reason : $what") 122 | false 123 | } 124 | 125 | binding.layoutSurfaceView.setOnTouchListener { _, event -> 126 | gestureDetector.onTouchEvent(event) 127 | true 128 | } 129 | 130 | binding.handlerTop.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 131 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 132 | onPlayerIndicatorSeekChanged(progress, fromUser) 133 | } 134 | 135 | override fun onStartTrackingTouch(seekBar: SeekBar) { 136 | onPlayerIndicatorSeekStart() 137 | } 138 | 139 | override fun onStopTrackingTouch(seekBar: SeekBar) { 140 | onPlayerIndicatorSeekStop(seekBar) 141 | } 142 | }) 143 | 144 | binding.timeLineBar.addOnRangeSeekBarListener(object : OnRangeSeekBarListener { 145 | override fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 146 | } 147 | 148 | override fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 149 | binding.handlerTop.visibility = View.GONE 150 | onSeekThumbs(index, value) 151 | } 152 | 153 | override fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 154 | } 155 | 156 | override fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 157 | onStopSeekThumbs() 158 | } 159 | }) 160 | 161 | binding.videoLoader.setOnPreparedListener { mp -> onVideoPrepared(mp) } 162 | binding.videoLoader.setOnCompletionListener { onVideoCompleted() } 163 | } 164 | 165 | private fun onPlayerIndicatorSeekChanged(progress: Int, fromUser: Boolean) { 166 | val duration = (mDuration * progress / 1000L) 167 | if (fromUser) { 168 | if (duration < mStartPosition) setProgressBarPosition(mStartPosition) 169 | else if (duration > mEndPosition) setProgressBarPosition(mEndPosition) 170 | } 171 | } 172 | 173 | private fun onPlayerIndicatorSeekStart() { 174 | mMessageHandler.removeMessages(SHOW_PROGRESS) 175 | binding.videoLoader.pause() 176 | binding.iconVideoPlay.visibility = View.VISIBLE 177 | notifyProgressUpdate(false) 178 | } 179 | 180 | private fun onPlayerIndicatorSeekStop(seekBar: SeekBar) { 181 | mMessageHandler.removeMessages(SHOW_PROGRESS) 182 | binding.videoLoader.pause() 183 | binding.iconVideoPlay.visibility = View.VISIBLE 184 | 185 | val duration = (mDuration * seekBar.progress / 1000L).toInt() 186 | binding.videoLoader.seekTo(duration) 187 | notifyProgressUpdate(false) 188 | } 189 | 190 | private fun setProgressBarPosition(position: Float) { 191 | if (mDuration > 0) binding.handlerTop.progress = (1000L * position / mDuration).toInt() 192 | } 193 | 194 | private fun setUpMargins() { 195 | val marge = binding.timeLineBar.thumbs[0].widthBitmap 196 | val lp = binding.timeLineView.layoutParams as LayoutParams 197 | lp.setMargins(marge, 0, marge, 0) 198 | binding.timeLineView.layoutParams = lp 199 | } 200 | 201 | fun onSaveClicked() { 202 | binding.iconVideoPlay.visibility = View.VISIBLE 203 | binding.videoLoader.pause() 204 | mOnVideoEditedListener?.onTrimStarted() 205 | 206 | val mediaMetadataRetriever = MediaMetadataRetriever() 207 | mediaMetadataRetriever.setDataSource(context, mSrc) 208 | val metaDataKeyDuration = 209 | java.lang.Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) 210 | 211 | val file = File(mSrc.path ?: "") 212 | 213 | if (mTimeVideo < MIN_TIME_FRAME) { 214 | if (metaDataKeyDuration - mEndPosition > MIN_TIME_FRAME - mTimeVideo) mEndPosition += MIN_TIME_FRAME - mTimeVideo 215 | else if (mStartPosition > MIN_TIME_FRAME - mTimeVideo) mStartPosition -= MIN_TIME_FRAME - mTimeVideo 216 | } 217 | 218 | val root = File(destinationPath) 219 | root.mkdirs() 220 | val outputFileUri = Uri.fromFile( 221 | File( 222 | root, 223 | "t_${Calendar.getInstance().timeInMillis}_" + file.nameWithoutExtension + ".mp4" 224 | ) 225 | ) 226 | val outPutPath = RealPathUtil.realPathFromUriApi19(context, outputFileUri) 227 | ?: File( 228 | root, 229 | "t_${Calendar.getInstance().timeInMillis}_" + mSrc.path?.substring( 230 | mSrc.path!!.lastIndexOf("/") + 1 231 | ) 232 | ).absolutePath 233 | Log.e("SOURCE", file.path) 234 | Log.e("DESTINATION", outPutPath) 235 | val extractor = MediaExtractor() 236 | var frameRate = 24 237 | try { 238 | extractor.setDataSource(file.path) 239 | val numTracks = extractor.trackCount 240 | for (i in 0..numTracks) { 241 | val format = extractor.getTrackFormat(i) 242 | val mime = format.getString(MediaFormat.KEY_MIME) 243 | if (mime?.startsWith("video/") == true) { 244 | if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) { 245 | frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE) 246 | } 247 | } 248 | } 249 | } catch (e: Exception) { 250 | e.printStackTrace() 251 | } finally { 252 | extractor.release() 253 | } 254 | val duration = 255 | java.lang.Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) 256 | Log.e("FRAME RATE", frameRate.toString()) 257 | Log.e("FRAME COUNT", (duration / 1000 * frameRate).toString()) 258 | 259 | handleVideoCrop(file) 260 | } 261 | 262 | private fun handleVideoCrop(file: File) { 263 | val rect = binding.cropFrame.cropRect ?: return 264 | var width = abs(rect.left - rect.right) 265 | var height = abs(rect.top - rect.bottom) 266 | var x = rect.left 267 | var y = rect.top 268 | val root = File(destinationPath) 269 | root.mkdirs() 270 | val outputFileUri = Uri.fromFile( 271 | File( 272 | root, 273 | "t_${Calendar.getInstance().timeInMillis}_" + file.nameWithoutExtension + ".mp4" 274 | ) 275 | ) 276 | val outPutPath = RealPathUtil.realPathFromUriApi19(context, outputFileUri) 277 | ?: File( 278 | root, 279 | "t_${Calendar.getInstance().timeInMillis}_" + mSrc.path?.substring( 280 | mSrc.path!!.lastIndexOf("/") + 1 281 | ) 282 | ).absolutePath 283 | val extractor = MediaExtractor() 284 | var frameRate = 24 285 | try { 286 | extractor.setDataSource(file.absolutePath) 287 | val numTracks = extractor.trackCount 288 | for (i in 0..numTracks) { 289 | val format = extractor.getTrackFormat(i) 290 | val mime = format.getString(MediaFormat.KEY_MIME) 291 | if (mime?.startsWith("video/") == true) { 292 | if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) { 293 | frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE) 294 | } 295 | } 296 | } 297 | } catch (e: Exception) { 298 | e.printStackTrace() 299 | } finally { 300 | extractor.release() 301 | } 302 | val mediaMetadataRetriever = MediaMetadataRetriever() 303 | mediaMetadataRetriever.setDataSource(context, mSrc) 304 | 305 | val bitRate = 306 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) 307 | ?.toDouble() ?: MIN_BITRATE 308 | 309 | val duration = 310 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() 311 | 312 | val xPercentage = (x * 100) / videoPlayerWidth 313 | val yPercentage = (y * 100) / videoPlayerHeight 314 | 315 | width = (width * originalVideoWidth) / videoPlayerWidth 316 | x = originalVideoWidth * xPercentage / 100 317 | 318 | height = (height * originalVideoHeight) / videoPlayerHeight 319 | y = originalVideoHeight * (yPercentage + 1) / 100 320 | 321 | // run in IO Thread 322 | findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.IO) { 323 | VideoOptions(context).trimAndCropVideo( 324 | width = width, 325 | height = height, 326 | x = x, 327 | y = y, 328 | bitrate = getBitrate(bitRate.bitToMb(), videoQuality), 329 | startPosition = TrimVideoUtils.stringForTime(mStartPosition), 330 | endPosition = TrimVideoUtils.stringForTime(mEndPosition), 331 | inputPath = file.path, 332 | outputPath = outPutPath, 333 | outputFileUri = outputFileUri, 334 | listener = mOnVideoEditedListener, 335 | totalVideoDuration = duration ?: 0 336 | ) 337 | } 338 | } 339 | 340 | /** 341 | * Get fixed bitrate value based on the file's current bitrate 342 | * @param bitrate file's current bitrate 343 | * @return new smaller bitrate value 344 | */ 345 | private fun getBitrate( 346 | bitrate: Double, 347 | quality: VideoQuality, 348 | ): Double { 349 | if (bitrate < 1) return 1.0 350 | return when (quality) { 351 | VideoQuality.Low -> (bitrate * 0.2) 352 | VideoQuality.Medium -> (bitrate * 0.3) 353 | VideoQuality.High -> (bitrate * 0.4) 354 | VideoQuality.VeryHigh -> (bitrate * 0.6) 355 | } 356 | } 357 | 358 | private fun onClickVideoPlayPause() { 359 | if (binding.videoLoader.isPlaying) { 360 | binding.iconVideoPlay.visibility = View.VISIBLE 361 | mMessageHandler.removeMessages(SHOW_PROGRESS) 362 | binding.videoLoader.pause() 363 | } else { 364 | binding.iconVideoPlay.visibility = View.GONE 365 | if (mResetSeekBar) { 366 | mResetSeekBar = false 367 | binding.videoLoader.seekTo(mStartPosition.toInt()) 368 | } 369 | mMessageHandler.sendEmptyMessage(SHOW_PROGRESS) 370 | binding.videoLoader.start() 371 | } 372 | } 373 | 374 | fun onCancelClicked() { 375 | binding.videoLoader.stopPlayback() 376 | mOnVideoEditedListener?.cancelAction() 377 | } 378 | 379 | private fun onVideoPrepared(mp: MediaPlayer) { 380 | if (isVideoPrepared) return 381 | isVideoPrepared = true 382 | val videoWidth = mp.videoWidth 383 | val videoHeight = mp.videoHeight 384 | val videoProportion = videoWidth.toFloat() / videoHeight.toFloat() 385 | val screenWidth = binding.layoutSurfaceView.width 386 | val screenHeight = binding.layoutSurfaceView.height 387 | val screenProportion = screenWidth.toFloat() / screenHeight.toFloat() 388 | val lp = binding.videoLoader.layoutParams 389 | 390 | if (videoProportion > screenProportion) { 391 | lp.width = screenWidth 392 | lp.height = (screenWidth.toFloat() / videoProportion).toInt() 393 | } else { 394 | lp.width = (videoProportion * screenHeight.toFloat()).toInt() 395 | lp.height = screenHeight 396 | } 397 | videoPlayerWidth = lp.width 398 | videoPlayerHeight = lp.height 399 | binding.videoLoader.layoutParams = lp 400 | setupCropper(lp.width, lp.height) 401 | 402 | binding.iconVideoPlay.visibility = View.VISIBLE 403 | mDuration = binding.videoLoader.duration.toFloat() 404 | setSeekBarPosition() 405 | setTimeFrames() 406 | mOnVideoListener?.onVideoPrepared() 407 | 408 | } 409 | 410 | private fun setSeekBarPosition() { 411 | when { 412 | mDuration >= mMaxDuration && mMaxDuration != -1 -> { 413 | mStartPosition = mDuration / 2 - mMaxDuration / 2 414 | mEndPosition = mDuration / 2 + mMaxDuration / 2 415 | binding.timeLineBar.setThumbValue(0, (mStartPosition * 100 / mDuration)) 416 | binding.timeLineBar.setThumbValue(1, (mEndPosition * 100 / mDuration)) 417 | } 418 | 419 | mDuration <= mMinDuration && mMinDuration != -1 -> { 420 | mStartPosition = mDuration / 2 - mMinDuration / 2 421 | mEndPosition = mDuration / 2 + mMinDuration / 2 422 | binding.timeLineBar.setThumbValue(0, (mStartPosition * 100 / mDuration)) 423 | binding.timeLineBar.setThumbValue(1, (mEndPosition * 100 / mDuration)) 424 | } 425 | 426 | else -> { 427 | mStartPosition = 0f 428 | mEndPosition = mDuration 429 | } 430 | } 431 | binding.videoLoader.seekTo(mStartPosition.toInt()) 432 | mTimeVideo = mDuration 433 | binding.timeLineBar.initMaxWidth() 434 | } 435 | 436 | private fun setTimeFrames() { 437 | val seconds = context.getString(R.string.short_seconds) 438 | binding.textTimeSelection.text = String.format( 439 | Locale.ENGLISH, 440 | "%s %s - %s %s", 441 | TrimVideoUtils.stringForTime(mStartPosition), 442 | seconds, 443 | TrimVideoUtils.stringForTime(mEndPosition), 444 | seconds 445 | ) 446 | } 447 | 448 | private fun onSeekThumbs(index: Int, value: Float) { 449 | when (index) { 450 | Thumb.LEFT -> { 451 | mStartPosition = (mDuration * value / 100L) 452 | binding.videoLoader.seekTo(mStartPosition.toInt()) 453 | } 454 | 455 | Thumb.RIGHT -> { 456 | mEndPosition = (mDuration * value / 100L) 457 | } 458 | } 459 | setTimeFrames() 460 | mTimeVideo = mEndPosition - mStartPosition 461 | } 462 | 463 | private fun onStopSeekThumbs() { 464 | mMessageHandler.removeMessages(SHOW_PROGRESS) 465 | binding.videoLoader.pause() 466 | binding.iconVideoPlay.visibility = View.VISIBLE 467 | } 468 | 469 | private fun onVideoCompleted() { 470 | binding.videoLoader.seekTo(mStartPosition.toInt()) 471 | } 472 | 473 | private fun notifyProgressUpdate(all: Boolean) { 474 | if (mDuration == 0f) return 475 | val position = binding.videoLoader.currentPosition 476 | if (all) { 477 | for (item in mListeners) { 478 | item.updateProgress(position.toFloat(), mDuration, (position * 100 / mDuration)) 479 | } 480 | } else { 481 | mListeners[0].updateProgress( 482 | position.toFloat(), 483 | mDuration, 484 | (position * 100 / mDuration) 485 | ) 486 | } 487 | } 488 | 489 | private fun updateVideoProgress(time: Float) { 490 | if (binding.videoLoader == null) return 491 | if (time <= mStartPosition && time <= mEndPosition) binding.handlerTop.visibility = 492 | View.GONE 493 | else binding.handlerTop.visibility = View.VISIBLE 494 | if (time >= mEndPosition) { 495 | mMessageHandler.removeMessages(SHOW_PROGRESS) 496 | binding.videoLoader.pause() 497 | binding.iconVideoPlay.visibility = View.VISIBLE 498 | mResetSeekBar = true 499 | return 500 | } 501 | setProgressBarPosition(time) 502 | } 503 | 504 | fun setBitrate(bitRate: Int): VideoEditor { 505 | this.bitRate = bitRate 506 | return this 507 | } 508 | 509 | fun setVideoInformationVisibility(visible: Boolean): VideoEditor { 510 | binding.timeFrame.visibility = if (visible) View.VISIBLE else View.GONE 511 | return this 512 | } 513 | 514 | fun setOnTrimVideoListener(onVideoEditedListener: OnVideoEditedListener): VideoEditor { 515 | mOnVideoEditedListener = onVideoEditedListener 516 | return this 517 | } 518 | 519 | fun setOnVideoListener(onVideoListener: OnVideoListener): VideoEditor { 520 | mOnVideoListener = onVideoListener 521 | return this 522 | } 523 | 524 | fun destroy() { 525 | BackgroundExecutor.cancelAll("", true) 526 | UiThreadExecutor.cancelAll("") 527 | } 528 | 529 | fun setMaxDuration(maxDuration: Int): VideoEditor { 530 | mMaxDuration = maxDuration * 1000 531 | return this 532 | } 533 | 534 | fun setMinDuration(minDuration: Int): VideoEditor { 535 | mMinDuration = minDuration * 1000 536 | return this 537 | } 538 | 539 | fun setDestinationPath(path: String): VideoEditor { 540 | destinationPath = path 541 | return this 542 | } 543 | 544 | fun setVideoQuality(videoQuality: VideoQuality): VideoEditor { 545 | this.videoQuality = videoQuality 546 | return this 547 | } 548 | 549 | fun setVideoURI(videoURI: Uri): VideoEditor { 550 | mSrc = videoURI 551 | binding.videoLoader.setVideoURI(mSrc) 552 | binding.videoLoader.requestFocus() 553 | binding.timeLineView.setVideo(mSrc) 554 | val mediaMetadataRetriever = MediaMetadataRetriever() 555 | mediaMetadataRetriever.setDataSource(context, mSrc) 556 | val metaDateWidth = 557 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) 558 | ?.toInt() ?: 0 559 | val metaDataHeight = 560 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) 561 | ?.toInt() ?: 0 562 | 563 | //If the rotation is 90 or 270 the width and height will be transposed. 564 | when (mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) 565 | ?.toInt()) { 566 | 90, 270 -> { 567 | originalVideoWidth = metaDataHeight 568 | originalVideoHeight = metaDateWidth 569 | } 570 | 571 | else -> { 572 | originalVideoWidth = metaDateWidth 573 | originalVideoHeight = metaDataHeight 574 | } 575 | } 576 | 577 | return this 578 | } 579 | 580 | private fun setupCropper(width: Int, height: Int) { 581 | binding.cropFrame.setFixedAspectRatio(false) 582 | binding.cropFrame.layoutParams = binding.cropFrame.layoutParams?.let { 583 | it.width = width 584 | it.height = height 585 | it 586 | } 587 | binding.cropFrame.setImageBitmap( 588 | context.getDrawable(android.R.color.transparent) 589 | ?.let { drawableToBitmap(it, width, height) }) 590 | } 591 | 592 | fun setTextTimeSelectionTypeface(tf: Typeface?): VideoEditor { 593 | if (tf != null) binding.textTimeSelection.typeface = tf 594 | return this 595 | } 596 | 597 | fun onResume() { 598 | binding.videoLoader.seekTo(videoPlayerCurrentPosition) 599 | } 600 | 601 | fun onPause() { 602 | videoPlayerCurrentPosition = binding.videoLoader.currentPosition 603 | } 604 | 605 | private class MessageHandler(view: VideoEditor) : Handler() { 606 | private val mView: WeakReference = WeakReference(view) 607 | override fun handleMessage(msg: Message) { 608 | val view = mView.get() 609 | if (view == null || view.binding.videoLoader == null) return 610 | view.notifyProgressUpdate(true) 611 | if (view.binding.videoLoader.isPlaying) sendEmptyMessageDelayed(0, 10) 612 | } 613 | } 614 | 615 | companion object { 616 | private const val MIN_TIME_FRAME = 1000 617 | private const val SHOW_PROGRESS = 2 618 | private const val MIN_BITRATE = 1500000.0 619 | 620 | } 621 | } 622 | 623 | private fun Double.bitToMb() = this / 1000000 624 | 625 | -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable-hdpi/ic_play_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/video-editor/src/main/res/drawable-hdpi/ic_play_video.png -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable-hdpi/seek_left_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/video-editor/src/main/res/drawable-hdpi/seek_left_handle.png -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable-hdpi/seek_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/video-editor/src/main/res/drawable-hdpi/seek_line.png -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable-hdpi/seek_middle_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/video-editor/src/main/res/drawable-hdpi/seek_middle_handle.png -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable-hdpi/seek_right_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizisdeepan/VideoEditor/8c33331aa1a5b5bd632cfbc906d1366bc6a24160/video-editor/src/main/res/drawable-hdpi/seek_right_handle.png -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable-v21/play_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable/play_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable/rounded_textview_video_trim.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /video-editor/src/main/res/drawable/seekbar_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /video-editor/src/main/res/layout/view_cropper.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 25 | 26 | 35 | 36 | 41 | 42 | 53 | 54 | 55 | 56 | 66 | 67 | 72 | 73 | 77 | 78 | 79 | 80 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /video-editor/src/main/res/layout/view_trimmer.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 23 | 24 | 35 | 36 | 43 | 44 | 45 | 46 | 52 | 53 | 63 | 64 | 76 | 77 | 78 | 79 | 86 | 87 | 93 | 94 | 101 | 102 | 103 | 104 | 118 | 119 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | الغاء 3 | حفظ 4 | sec 5 | MB 6 | KB 7 | 8 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Cancelar 3 | Guardar 4 | sec 5 | MB 6 | KB 7 | 8 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Cancelar 3 | Salvar 4 | sec 5 | MB 6 | KB 7 | 8 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw1080dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -3.60dp 4 | -7.20dp 5 | -10.80dp 6 | -14.40dp 7 | -18.00dp 8 | -21.60dp 9 | -25.20dp 10 | -28.80dp 11 | -32.40dp 12 | -36.00dp 13 | -39.60dp 14 | -43.20dp 15 | -46.80dp 16 | -50.40dp 17 | -54.00dp 18 | -57.60dp 19 | -61.20dp 20 | -64.80dp 21 | -68.40dp 22 | -72.00dp 23 | -75.60dp 24 | -79.20dp 25 | -82.80dp 26 | -86.40dp 27 | -90.00dp 28 | -93.60dp 29 | -97.20dp 30 | -100.80dp 31 | -104.40dp 32 | -108.00dp 33 | -111.60dp 34 | -115.20dp 35 | -118.80dp 36 | -122.40dp 37 | -126.00dp 38 | -129.60dp 39 | -133.20dp 40 | -136.80dp 41 | -140.40dp 42 | -144.00dp 43 | -147.60dp 44 | -151.20dp 45 | -154.80dp 46 | -158.40dp 47 | -162.00dp 48 | -165.60dp 49 | -169.20dp 50 | -172.80dp 51 | -176.40dp 52 | -180.00dp 53 | -183.60dp 54 | -187.20dp 55 | -190.80dp 56 | -194.40dp 57 | -198.00dp 58 | -201.60dp 59 | -205.20dp 60 | -208.80dp 61 | -212.40dp 62 | -216.00dp 63 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw300dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.00dp 4 | -2.00dp 5 | -3.00dp 6 | -4.00dp 7 | -5.00dp 8 | -6.00dp 9 | -7.00dp 10 | -8.00dp 11 | -9.00dp 12 | -10.00dp 13 | -11.00dp 14 | -12.00dp 15 | -13.00dp 16 | -14.00dp 17 | -15.00dp 18 | -16.00dp 19 | -17.00dp 20 | -18.00dp 21 | -19.00dp 22 | -20.00dp 23 | -21.00dp 24 | -22.00dp 25 | -23.00dp 26 | -24.00dp 27 | -25.00dp 28 | -26.00dp 29 | -27.00dp 30 | -28.00dp 31 | -29.00dp 32 | -30.00dp 33 | -31.00dp 34 | -32.00dp 35 | -33.00dp 36 | -34.00dp 37 | -35.00dp 38 | -36.00dp 39 | -37.00dp 40 | -38.00dp 41 | -39.00dp 42 | -40.00dp 43 | -41.00dp 44 | -42.00dp 45 | -43.00dp 46 | -44.00dp 47 | -45.00dp 48 | -46.00dp 49 | -47.00dp 50 | -48.00dp 51 | -49.00dp 52 | -50.00dp 53 | -51.00dp 54 | -52.00dp 55 | -53.00dp 56 | -54.00dp 57 | -55.00dp 58 | -56.00dp 59 | -57.00dp 60 | -58.00dp 61 | -59.00dp 62 | -60.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw330dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.10dp 4 | -2.20dp 5 | -3.30dp 6 | -4.40dp 7 | -5.50dp 8 | -6.60dp 9 | -7.70dp 10 | -8.80dp 11 | -9.90dp 12 | -11.00dp 13 | -12.10dp 14 | -13.20dp 15 | -14.30dp 16 | -15.40dp 17 | -16.50dp 18 | -17.60dp 19 | -18.70dp 20 | -19.80dp 21 | -20.90dp 22 | -22.00dp 23 | -23.10dp 24 | -24.20dp 25 | -25.30dp 26 | -26.40dp 27 | -27.50dp 28 | -28.60dp 29 | -29.70dp 30 | -30.80dp 31 | -31.90dp 32 | -33.00dp 33 | -34.10dp 34 | -35.20dp 35 | -36.30dp 36 | -37.40dp 37 | -38.50dp 38 | -39.60dp 39 | -40.70dp 40 | -41.80dp 41 | -42.90dp 42 | -44.00dp 43 | -45.10dp 44 | -46.20dp 45 | -47.30dp 46 | -48.40dp 47 | -49.50dp 48 | -50.60dp 49 | -51.70dp 50 | -52.80dp 51 | -53.90dp 52 | -55.00dp 53 | -56.10dp 54 | -57.20dp 55 | -58.30dp 56 | -59.40dp 57 | -60.50dp 58 | -61.60dp 59 | -62.70dp 60 | -63.80dp 61 | -64.90dp 62 | -66.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw360dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.20dp 4 | -2.40dp 5 | -3.60dp 6 | -4.80dp 7 | -6.00dp 8 | -7.20dp 9 | -8.40dp 10 | -9.60dp 11 | -10.80dp 12 | -12.00dp 13 | -13.20dp 14 | -14.40dp 15 | -15.60dp 16 | -16.80dp 17 | -18.00dp 18 | -19.20dp 19 | -20.40dp 20 | -21.60dp 21 | -22.80dp 22 | -24.00dp 23 | -25.20dp 24 | -26.40dp 25 | -27.60dp 26 | -28.80dp 27 | -30.00dp 28 | -31.20dp 29 | -32.40dp 30 | -33.60dp 31 | -34.80dp 32 | -36.00dp 33 | -37.20dp 34 | -38.40dp 35 | -39.60dp 36 | -40.80dp 37 | -42.00dp 38 | -43.20dp 39 | -44.40dp 40 | -45.60dp 41 | -46.80dp 42 | -48.00dp 43 | -49.20dp 44 | -50.40dp 45 | -51.60dp 46 | -52.80dp 47 | -54.00dp 48 | -55.20dp 49 | -56.40dp 50 | -57.60dp 51 | -58.80dp 52 | -60.00dp 53 | -61.20dp 54 | -62.40dp 55 | -63.60dp 56 | -64.80dp 57 | -66.00dp 58 | -67.20dp 59 | -68.40dp 60 | -69.60dp 61 | -70.80dp 62 | -72.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw390dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.30dp 4 | -2.60dp 5 | -3.90dp 6 | -5.20dp 7 | -6.50dp 8 | -7.80dp 9 | -9.10dp 10 | -10.40dp 11 | -11.70dp 12 | -13.00dp 13 | -14.30dp 14 | -15.60dp 15 | -16.90dp 16 | -18.20dp 17 | -19.50dp 18 | -20.80dp 19 | -22.10dp 20 | -23.40dp 21 | -24.70dp 22 | -26.00dp 23 | -27.30dp 24 | -28.60dp 25 | -29.90dp 26 | -31.20dp 27 | -32.50dp 28 | -33.80dp 29 | -35.10dp 30 | -36.40dp 31 | -37.70dp 32 | -39.00dp 33 | -40.30dp 34 | -41.60dp 35 | -42.90dp 36 | -44.20dp 37 | -45.50dp 38 | -46.80dp 39 | -48.10dp 40 | -49.40dp 41 | -50.70dp 42 | -52.00dp 43 | -53.30dp 44 | -54.60dp 45 | -55.90dp 46 | -57.20dp 47 | -58.50dp 48 | -59.80dp 49 | -61.10dp 50 | -62.40dp 51 | -63.70dp 52 | -65.00dp 53 | -66.30dp 54 | -67.60dp 55 | -68.90dp 56 | -70.20dp 57 | -71.50dp 58 | -72.80dp 59 | -74.10dp 60 | -75.40dp 61 | -76.70dp 62 | -78.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw420dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.40dp 4 | -2.80dp 5 | -4.20dp 6 | -5.60dp 7 | -7.00dp 8 | -8.40dp 9 | -9.80dp 10 | -11.20dp 11 | -12.60dp 12 | -14.00dp 13 | -15.40dp 14 | -16.80dp 15 | -18.20dp 16 | -19.60dp 17 | -21.00dp 18 | -22.40dp 19 | -23.80dp 20 | -25.20dp 21 | -26.60dp 22 | -28.00dp 23 | -29.40dp 24 | -30.80dp 25 | -32.20dp 26 | -33.60dp 27 | -35.00dp 28 | -36.40dp 29 | -37.80dp 30 | -39.20dp 31 | -40.60dp 32 | -42.00dp 33 | -43.40dp 34 | -44.80dp 35 | -46.20dp 36 | -47.60dp 37 | -49.00dp 38 | -50.40dp 39 | -51.80dp 40 | -53.20dp 41 | -54.60dp 42 | -56.00dp 43 | -57.40dp 44 | -58.80dp 45 | -60.20dp 46 | -61.60dp 47 | -63.00dp 48 | -64.40dp 49 | -65.80dp 50 | -67.20dp 51 | -68.60dp 52 | -70.00dp 53 | -71.40dp 54 | -72.80dp 55 | -74.20dp 56 | -75.60dp 57 | -77.00dp 58 | -78.40dp 59 | -79.80dp 60 | -81.20dp 61 | -82.60dp 62 | -84.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw450dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.50dp 4 | -3.00dp 5 | -4.50dp 6 | -6.00dp 7 | -7.50dp 8 | -9.00dp 9 | -10.50dp 10 | -12.00dp 11 | -13.50dp 12 | -15.00dp 13 | -16.50dp 14 | -18.00dp 15 | -19.50dp 16 | -21.00dp 17 | -22.50dp 18 | -24.00dp 19 | -25.50dp 20 | -27.00dp 21 | -28.50dp 22 | -30.00dp 23 | -31.50dp 24 | -33.00dp 25 | -34.50dp 26 | -36.00dp 27 | -37.50dp 28 | -39.00dp 29 | -40.50dp 30 | -42.00dp 31 | -43.50dp 32 | -45.00dp 33 | -46.50dp 34 | -48.00dp 35 | -49.50dp 36 | -51.00dp 37 | -52.50dp 38 | -54.00dp 39 | -55.50dp 40 | -57.00dp 41 | -58.50dp 42 | -60.00dp 43 | -61.50dp 44 | -63.00dp 45 | -64.50dp 46 | -66.00dp 47 | -67.50dp 48 | -69.00dp 49 | -70.50dp 50 | -72.00dp 51 | -73.50dp 52 | -75.00dp 53 | -76.50dp 54 | -78.00dp 55 | -79.50dp 56 | -81.00dp 57 | -82.50dp 58 | -84.00dp 59 | -85.50dp 60 | -87.00dp 61 | -88.50dp 62 | -90.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw480dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.60dp 4 | -3.20dp 5 | -4.80dp 6 | -6.40dp 7 | -8.00dp 8 | -9.60dp 9 | -11.20dp 10 | -12.80dp 11 | -14.40dp 12 | -16.00dp 13 | -17.60dp 14 | -19.20dp 15 | -20.80dp 16 | -22.40dp 17 | -24.00dp 18 | -25.60dp 19 | -27.20dp 20 | -28.80dp 21 | -30.40dp 22 | -32.00dp 23 | -33.60dp 24 | -35.20dp 25 | -36.80dp 26 | -38.40dp 27 | -40.00dp 28 | -41.60dp 29 | -43.20dp 30 | -44.80dp 31 | -46.40dp 32 | -48.00dp 33 | -49.60dp 34 | -51.20dp 35 | -52.80dp 36 | -54.40dp 37 | -56.00dp 38 | -57.60dp 39 | -59.20dp 40 | -60.80dp 41 | -62.40dp 42 | -64.00dp 43 | -65.60dp 44 | -67.20dp 45 | -68.80dp 46 | -70.40dp 47 | -72.00dp 48 | -73.60dp 49 | -75.20dp 50 | -76.80dp 51 | -78.40dp 52 | -80.00dp 53 | -81.60dp 54 | -83.20dp 55 | -84.80dp 56 | -86.40dp 57 | -88.00dp 58 | -89.60dp 59 | -91.20dp 60 | -92.80dp 61 | -94.40dp 62 | -96.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw510dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.70dp 4 | -3.40dp 5 | -5.10dp 6 | -6.80dp 7 | -8.50dp 8 | -10.20dp 9 | -11.90dp 10 | -13.60dp 11 | -15.30dp 12 | -17.00dp 13 | -18.70dp 14 | -20.40dp 15 | -22.10dp 16 | -23.80dp 17 | -25.50dp 18 | -27.20dp 19 | -28.90dp 20 | -30.60dp 21 | -32.30dp 22 | -34.00dp 23 | -35.70dp 24 | -37.40dp 25 | -39.10dp 26 | -40.80dp 27 | -42.50dp 28 | -44.20dp 29 | -45.90dp 30 | -47.60dp 31 | -49.30dp 32 | -51.00dp 33 | -52.70dp 34 | -54.40dp 35 | -56.10dp 36 | -57.80dp 37 | -59.50dp 38 | -61.20dp 39 | -62.90dp 40 | -64.60dp 41 | -66.30dp 42 | -68.00dp 43 | -69.70dp 44 | -71.40dp 45 | -73.10dp 46 | -74.80dp 47 | -76.50dp 48 | -78.20dp 49 | -79.90dp 50 | -81.60dp 51 | -83.30dp 52 | -85.00dp 53 | -86.70dp 54 | -88.40dp 55 | -90.10dp 56 | -91.80dp 57 | -93.50dp 58 | -95.20dp 59 | -96.90dp 60 | -98.60dp 61 | -100.30dp 62 | -102.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw540dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.80dp 4 | -3.60dp 5 | -5.40dp 6 | -7.20dp 7 | -9.00dp 8 | -10.80dp 9 | -12.60dp 10 | -14.40dp 11 | -16.20dp 12 | -18.00dp 13 | -19.80dp 14 | -21.60dp 15 | -23.40dp 16 | -25.20dp 17 | -27.00dp 18 | -28.80dp 19 | -30.60dp 20 | -32.40dp 21 | -34.20dp 22 | -36.00dp 23 | -37.80dp 24 | -39.60dp 25 | -41.40dp 26 | -43.20dp 27 | -45.00dp 28 | -46.80dp 29 | -48.60dp 30 | -50.40dp 31 | -52.20dp 32 | -54.00dp 33 | -55.80dp 34 | -57.60dp 35 | -59.40dp 36 | -61.20dp 37 | -63.00dp 38 | -64.80dp 39 | -66.60dp 40 | -68.40dp 41 | -70.20dp 42 | -72.00dp 43 | -73.80dp 44 | -75.60dp 45 | -77.40dp 46 | -79.20dp 47 | -81.00dp 48 | -82.80dp 49 | -84.60dp 50 | -86.40dp 51 | -88.20dp 52 | -90.00dp 53 | -91.80dp 54 | -93.60dp 55 | -95.40dp 56 | -97.20dp 57 | -99.00dp 58 | -100.80dp 59 | -102.60dp 60 | -104.40dp 61 | -106.20dp 62 | -108.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw570dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -1.90dp 4 | -3.80dp 5 | -5.70dp 6 | -7.60dp 7 | -9.50dp 8 | -11.40dp 9 | -13.30dp 10 | -15.20dp 11 | -17.10dp 12 | -19.00dp 13 | -20.90dp 14 | -22.80dp 15 | -24.70dp 16 | -26.60dp 17 | -28.50dp 18 | -30.40dp 19 | -32.30dp 20 | -34.20dp 21 | -36.10dp 22 | -38.00dp 23 | -39.90dp 24 | -41.80dp 25 | -43.70dp 26 | -45.60dp 27 | -47.50dp 28 | -49.40dp 29 | -51.30dp 30 | -53.20dp 31 | -55.10dp 32 | -57.00dp 33 | -58.90dp 34 | -60.80dp 35 | -62.70dp 36 | -64.60dp 37 | -66.50dp 38 | -68.40dp 39 | -70.30dp 40 | -72.20dp 41 | -74.10dp 42 | -76.00dp 43 | -77.90dp 44 | -79.80dp 45 | -81.70dp 46 | -83.60dp 47 | -85.50dp 48 | -87.40dp 49 | -89.30dp 50 | -91.20dp 51 | -93.10dp 52 | -95.00dp 53 | -96.90dp 54 | -98.80dp 55 | -100.70dp 56 | -102.60dp 57 | -104.50dp 58 | -106.40dp 59 | -108.30dp 60 | -110.20dp 61 | -112.10dp 62 | -114.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw600dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.00dp 4 | -4.00dp 5 | -6.00dp 6 | -8.00dp 7 | -10.00dp 8 | -12.00dp 9 | -14.00dp 10 | -16.00dp 11 | -18.00dp 12 | -20.00dp 13 | -22.00dp 14 | -24.00dp 15 | -26.00dp 16 | -28.00dp 17 | -30.00dp 18 | -32.00dp 19 | -34.00dp 20 | -36.00dp 21 | -38.00dp 22 | -40.00dp 23 | -42.00dp 24 | -44.00dp 25 | -46.00dp 26 | -48.00dp 27 | -50.00dp 28 | -52.00dp 29 | -54.00dp 30 | -56.00dp 31 | -58.00dp 32 | -60.00dp 33 | -62.00dp 34 | -64.00dp 35 | -66.00dp 36 | -68.00dp 37 | -70.00dp 38 | -72.00dp 39 | -74.00dp 40 | -76.00dp 41 | -78.00dp 42 | -80.00dp 43 | -82.00dp 44 | -84.00dp 45 | -86.00dp 46 | -88.00dp 47 | -90.00dp 48 | -92.00dp 49 | -94.00dp 50 | -96.00dp 51 | -98.00dp 52 | -100.00dp 53 | -102.00dp 54 | -104.00dp 55 | -106.00dp 56 | -108.00dp 57 | -110.00dp 58 | -112.00dp 59 | -114.00dp 60 | -116.00dp 61 | -118.00dp 62 | -120.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw630dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.10dp 4 | -4.20dp 5 | -6.30dp 6 | -8.40dp 7 | -10.50dp 8 | -12.60dp 9 | -14.70dp 10 | -16.80dp 11 | -18.90dp 12 | -21.00dp 13 | -23.10dp 14 | -25.20dp 15 | -27.30dp 16 | -29.40dp 17 | -31.50dp 18 | -33.60dp 19 | -35.70dp 20 | -37.80dp 21 | -39.90dp 22 | -42.00dp 23 | -44.10dp 24 | -46.20dp 25 | -48.30dp 26 | -50.40dp 27 | -52.50dp 28 | -54.60dp 29 | -56.70dp 30 | -58.80dp 31 | -60.90dp 32 | -63.00dp 33 | -65.10dp 34 | -67.20dp 35 | -69.30dp 36 | -71.40dp 37 | -73.50dp 38 | -75.60dp 39 | -77.70dp 40 | -79.80dp 41 | -81.90dp 42 | -84.00dp 43 | -86.10dp 44 | -88.20dp 45 | -90.30dp 46 | -92.40dp 47 | -94.50dp 48 | -96.60dp 49 | -98.70dp 50 | -100.80dp 51 | -102.90dp 52 | -105.00dp 53 | -107.10dp 54 | -109.20dp 55 | -111.30dp 56 | -113.40dp 57 | -115.50dp 58 | -117.60dp 59 | -119.70dp 60 | -121.80dp 61 | -123.90dp 62 | -126.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw660dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.20dp 4 | -4.40dp 5 | -6.60dp 6 | -8.80dp 7 | -11.00dp 8 | -13.20dp 9 | -15.40dp 10 | -17.60dp 11 | -19.80dp 12 | -22.00dp 13 | -24.20dp 14 | -26.40dp 15 | -28.60dp 16 | -30.80dp 17 | -33.00dp 18 | -35.20dp 19 | -37.40dp 20 | -39.60dp 21 | -41.80dp 22 | -44.00dp 23 | -46.20dp 24 | -48.40dp 25 | -50.60dp 26 | -52.80dp 27 | -55.00dp 28 | -57.20dp 29 | -59.40dp 30 | -61.60dp 31 | -63.80dp 32 | -66.00dp 33 | -68.20dp 34 | -70.40dp 35 | -72.60dp 36 | -74.80dp 37 | -77.00dp 38 | -79.20dp 39 | -81.40dp 40 | -83.60dp 41 | -85.80dp 42 | -88.00dp 43 | -90.20dp 44 | -92.40dp 45 | -94.60dp 46 | -96.80dp 47 | -99.00dp 48 | -101.20dp 49 | -103.40dp 50 | -105.60dp 51 | -107.80dp 52 | -110.00dp 53 | -112.20dp 54 | -114.40dp 55 | -116.60dp 56 | -118.80dp 57 | -121.00dp 58 | -123.20dp 59 | -125.40dp 60 | -127.60dp 61 | -129.80dp 62 | -132.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw690dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.30dp 4 | -4.60dp 5 | -6.90dp 6 | -9.20dp 7 | -11.50dp 8 | -13.80dp 9 | -16.10dp 10 | -18.40dp 11 | -20.70dp 12 | -23.00dp 13 | -25.30dp 14 | -27.60dp 15 | -29.90dp 16 | -32.20dp 17 | -34.50dp 18 | -36.80dp 19 | -39.10dp 20 | -41.40dp 21 | -43.70dp 22 | -46.00dp 23 | -48.30dp 24 | -50.60dp 25 | -52.90dp 26 | -55.20dp 27 | -57.50dp 28 | -59.80dp 29 | -62.10dp 30 | -64.40dp 31 | -66.70dp 32 | -69.00dp 33 | -71.30dp 34 | -73.60dp 35 | -75.90dp 36 | -78.20dp 37 | -80.50dp 38 | -82.80dp 39 | -85.10dp 40 | -87.40dp 41 | -89.70dp 42 | -92.00dp 43 | -94.30dp 44 | -96.60dp 45 | -98.90dp 46 | -101.20dp 47 | -103.50dp 48 | -105.80dp 49 | -108.10dp 50 | -110.40dp 51 | -112.70dp 52 | -115.00dp 53 | -117.30dp 54 | -119.60dp 55 | -121.90dp 56 | -124.20dp 57 | -126.50dp 58 | -128.80dp 59 | -131.10dp 60 | -133.40dp 61 | -135.70dp 62 | -138.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw720dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.40dp 4 | -4.80dp 5 | -7.20dp 6 | -9.60dp 7 | -12.00dp 8 | -14.40dp 9 | -16.80dp 10 | -19.20dp 11 | -21.60dp 12 | -24.00dp 13 | -26.40dp 14 | -28.80dp 15 | -31.20dp 16 | -33.60dp 17 | -36.00dp 18 | -38.40dp 19 | -40.80dp 20 | -43.20dp 21 | -45.60dp 22 | -48.00dp 23 | -50.40dp 24 | -52.80dp 25 | -55.20dp 26 | -57.60dp 27 | -60.00dp 28 | -62.40dp 29 | -64.80dp 30 | -67.20dp 31 | -69.60dp 32 | -72.00dp 33 | -74.40dp 34 | -76.80dp 35 | -79.20dp 36 | -81.60dp 37 | -84.00dp 38 | -86.40dp 39 | -88.80dp 40 | -91.20dp 41 | -93.60dp 42 | -96.00dp 43 | -98.40dp 44 | -100.80dp 45 | -103.20dp 46 | -105.60dp 47 | -108.00dp 48 | -110.40dp 49 | -112.80dp 50 | -115.20dp 51 | -117.60dp 52 | -120.00dp 53 | -122.40dp 54 | -124.80dp 55 | -127.20dp 56 | -129.60dp 57 | -132.00dp 58 | -134.40dp 59 | -136.80dp 60 | -139.20dp 61 | -141.60dp 62 | -144.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw750dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.50dp 4 | -5.00dp 5 | -7.50dp 6 | -10.00dp 7 | -12.50dp 8 | -15.00dp 9 | -17.50dp 10 | -20.00dp 11 | -22.50dp 12 | -25.00dp 13 | -27.50dp 14 | -30.00dp 15 | -32.50dp 16 | -35.00dp 17 | -37.50dp 18 | -40.00dp 19 | -42.50dp 20 | -45.00dp 21 | -47.50dp 22 | -50.00dp 23 | -52.50dp 24 | -55.00dp 25 | -57.50dp 26 | -60.00dp 27 | -62.50dp 28 | -65.00dp 29 | -67.50dp 30 | -70.00dp 31 | -72.50dp 32 | -75.00dp 33 | -77.50dp 34 | -80.00dp 35 | -82.50dp 36 | -85.00dp 37 | -87.50dp 38 | -90.00dp 39 | -92.50dp 40 | -95.00dp 41 | -97.50dp 42 | -100.00dp 43 | -102.50dp 44 | -105.00dp 45 | -107.50dp 46 | -110.00dp 47 | -112.50dp 48 | -115.00dp 49 | -117.50dp 50 | -120.00dp 51 | -122.50dp 52 | -125.00dp 53 | -127.50dp 54 | -130.00dp 55 | -132.50dp 56 | -135.00dp 57 | -137.50dp 58 | -140.00dp 59 | -142.50dp 60 | -145.00dp 61 | -147.50dp 62 | -150.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values-sw780dp/negative_sdps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -2.60dp 4 | -5.20dp 5 | -7.80dp 6 | -10.40dp 7 | -13.00dp 8 | -15.60dp 9 | -18.20dp 10 | -20.80dp 11 | -23.40dp 12 | -26.00dp 13 | -28.60dp 14 | -31.20dp 15 | -33.80dp 16 | -36.40dp 17 | -39.00dp 18 | -41.60dp 19 | -44.20dp 20 | -46.80dp 21 | -49.40dp 22 | -52.00dp 23 | -54.60dp 24 | -57.20dp 25 | -59.80dp 26 | -62.40dp 27 | -65.00dp 28 | -67.60dp 29 | -70.20dp 30 | -72.80dp 31 | -75.40dp 32 | -78.00dp 33 | -80.60dp 34 | -83.20dp 35 | -85.80dp 36 | -88.40dp 37 | -91.00dp 38 | -93.60dp 39 | -96.20dp 40 | -98.80dp 41 | -101.40dp 42 | -104.00dp 43 | -106.60dp 44 | -109.20dp 45 | -111.80dp 46 | -114.40dp 47 | -117.00dp 48 | -119.60dp 49 | -122.20dp 50 | -124.80dp 51 | -127.40dp 52 | -130.00dp 53 | -132.60dp 54 | -135.20dp 55 | -137.80dp 56 | -140.40dp 57 | -143.00dp 58 | -145.60dp 59 | -148.20dp 60 | -150.80dp 61 | -153.40dp 62 | -156.00dp 63 | 64 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #000000 6 | 7 | 8 | #2f3031 9 | #6C6D6D 10 | #3F51B5 11 | #FF15FF00 12 | #FF000000 13 | #565758 14 | #50000000 15 | 16 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2dp 5 | 40dip 6 | @dimen/_10sdp 7 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Cancel 3 | Save 4 | sec 5 | MB 6 | KB 7 | 8 | -------------------------------------------------------------------------------- /video-editor/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | --------------------------------------------------------------------------------