├── .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 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
3 | [](https://jitpack.io/#mohamed0017/SimpleVideoEditor)
4 |
5 |
6 | .jpg) |
7 |
Video Editor |
8 |
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 |
--------------------------------------------------------------------------------