diff --git a/android/.idea/.name b/android/.idea/.name index 954f54d..7316a8e 100644 --- a/android/.idea/.name +++ b/android/.idea/.name @@ -1 +1 @@ -videotrimming \ No newline at end of file +_android \ No newline at end of file diff --git a/android/.idea/caches/build_file_checksums.ser b/android/.idea/caches/build_file_checksums.ser index 4e82222..d648fd3 100644 Binary files a/android/.idea/caches/build_file_checksums.ser and b/android/.idea/caches/build_file_checksums.ser differ diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml index 16dedfe..e587dd1 100644 --- a/android/.idea/gradle.xml +++ b/android/.idea/gradle.xml @@ -14,6 +14,7 @@ diff --git a/android/.idea/modules.xml b/android/.idea/modules.xml index f3fe456..0311e69 100644 --- a/android/.idea/modules.xml +++ b/android/.idea/modules.xml @@ -2,6 +2,7 @@ + diff --git a/android/build.gradle b/android/build.gradle index ed84168..da85fa4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -38,18 +38,29 @@ android { defaultConfig { minSdkVersion multiDexEnabled true + minSdkVersion 26 + targetSdkVersion 30 } lintOptions { disable 'InvalidPackage' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.github.AndroidDeveloperLB:VideoTrimmer:6' - compile 'com.android.support:multidex:1.0.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + + implementation 'com.intuit.sdp:sdp-android:1.0.6' + implementation 'com.intuit.ssp:ssp-android:1.0.6' + implementation 'com.googlecode.mp4parser:isoparser:1.1.18' + + implementation 'com.android.support:multidex:1.0.3' } diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1b73b33..df1e830 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -3,9 +3,22 @@ package="com.steelkiwi.videotrimming"> + + android:name=".VideoTrimmerActivity" + android:configChanges="orientation|screenSize" + android:launchMode="singleTask" + android:screenOrientation="fullSensor" + android:windowSoftInputMode="adjustPan"> + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/FileUtils.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/FileUtils.java deleted file mode 100644 index 6a5a26a..0000000 --- a/android/src/main/kotlin/com/steelkiwi/videotrimming/FileUtils.java +++ /dev/null @@ -1,168 +0,0 @@ -package com.steelkiwi.videotrimming; - - -import android.annotation.SuppressLint; -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.provider.DocumentsContract; -import android.provider.MediaStore; -import android.text.TextUtils; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -class FileUtils { - - String getPathFromUri(final Context context, final Uri uri) { - String path = getPathFromLocalUri(context, uri); - if (path == null) { - path = getPathFromRemoteUri(context, uri); - } - return path; - } - - @SuppressLint("NewApi") - private String getPathFromLocalUri(final Context context, final Uri uri) { - final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { - if (isExternalStorageDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; - } - } else if (isDownloadsDocument(uri)) { - final String id = DocumentsContract.getDocumentId(uri); - - if (!TextUtils.isEmpty(id)) { - try { - final Uri contentUri = - ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); - return getDataColumn(context, contentUri, null, null); - } catch (NumberFormatException e) { - return null; - } - } - - } else if (isMediaDocument(uri)) { - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } - - final String selection = "_id=?"; - final String[] selectionArgs = new String[] {split[1]}; - - return getDataColumn(context, contentUri, selection, selectionArgs); - } - } else if ("content".equalsIgnoreCase(uri.getScheme())) { - - // Return the remote address - if (isGooglePhotosUri(uri)) { - return uri.getLastPathSegment(); - } - - return getDataColumn(context, uri, null, null); - } else if ("file".equalsIgnoreCase(uri.getScheme())) { - return uri.getPath(); - } - - return null; - } - - private static String getDataColumn( - Context context, Uri uri, String selection, String[] selectionArgs) { - Cursor cursor = null; - - final String column = "_data"; - final String[] projection = {column}; - - try { - cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); - if (cursor != null && cursor.moveToFirst()) { - final int column_index = cursor.getColumnIndexOrThrow(column); - return cursor.getString(column_index); - } - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - private static String getPathFromRemoteUri(final Context context, final Uri uri) { - // The code below is why Java now has try-with-resources and the Files utility. - File file = null; - InputStream inputStream = null; - OutputStream outputStream = null; - boolean success = false; - try { - inputStream = context.getContentResolver().openInputStream(uri); - file = File.createTempFile("image_picker", "jpg", context.getCacheDir()); - outputStream = new FileOutputStream(file); - if (inputStream != null) { - copy(inputStream, outputStream); - success = true; - } - } catch (IOException ignored) { - } finally { - try { - if (inputStream != null) inputStream.close(); - } catch (IOException ignored) { - } - try { - if (outputStream != null) outputStream.close(); - } catch (IOException ignored) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - success = false; - } - } - return success ? file.getPath() : null; - } - - private static void copy(InputStream in, OutputStream out) throws IOException { - final byte[] buffer = new byte[4 * 1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - out.flush(); - } - - private static boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); - } - - private static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - - private static boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - private static boolean isGooglePhotosUri(Uri uri) { - return "com.google.android.apps.photos.content".equals(uri.getAuthority()); - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimDelegate.kt b/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimDelegate.kt index 4a8ee00..fa49542 100644 --- a/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimDelegate.kt +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimDelegate.kt @@ -2,8 +2,8 @@ package com.steelkiwi.videotrimming import android.app.Activity import android.content.Intent -import android.widget.Toast -import com.steelkiwi.videotrimming.trim.TrimmerActivity +import android.net.Uri +import android.webkit.MimeTypeMap import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry @@ -12,35 +12,65 @@ import java.io.File class VideoTrimDelegate(private var activity: Activity) : PluginRegistry.ActivityResultListener { private var pendingResult: MethodChannel.Result? = null - private val fileUtils: FileUtils = FileUtils() + private val VIDEO_TRIM = 101 + fun startTrim(call: MethodCall, result: MethodChannel.Result?) { val sourcePath = call.argument("source_path") val maxSeconds = call.argument("max_seconds") pendingResult = result - val intent = Intent(activity, TrimmerActivity::class.java) - intent.putExtra(TrimmerActivity.EXTRA_INPUT_URI, sourcePath) - intent.putExtra(TrimmerActivity.EXTRA_INPUT_MAX_SECONDS, maxSeconds) - // activity.startActivityForResult(intent, TrimmerActivity.REQUEST_VIDEO_TRIMMER) + val intent = Intent(activity, VideoTrimmerActivity::class.java) + intent.putExtra("EXTRA_PATH", sourcePath) +// intent.putExtra(TrimmerActivity.EXTRA_INPUT_MAX_SECONDS, maxSeconds) + val uriFile = Uri.fromFile(File(sourcePath)) + val fileExt = MimeTypeMap.getFileExtensionFromUrl(uriFile.toString()) + if (fileExt.equals("MP4", ignoreCase = true)) { + val file = File(sourcePath) + if (file.exists()) { + activity.startActivityForResult(intent, VIDEO_TRIM) + + } else { + } + } else { + } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - if (resultCode == Activity.RESULT_OK) { - if (requestCode == TrimmerActivity.REQUEST_VIDEO_TRIMMER) { - var data = data?.getStringExtra(TrimmerActivity.EXTRA_OUTPUT_FILE); + if (requestCode == VIDEO_TRIM) { + if (resultCode == Activity.RESULT_OK) { if (data != null) { - finishWithSuccess(data) - } else { - finishWithError("crop_error", "Output path null", Throwable(message = "Output path null")) - } - - return true + val videoPath = data.extras!!.getString("INTENT_VIDEO_FILE") + videoPath?.let { finishWithSuccess(it) } +// val file = File(videoPath) +// Log.d(TAG, "onActivityResult: " + file.length()) +// pathPostImg = videoPath +// Glide.with(this) +// .load(pathPostImg) +// .into(postImg) +// postImgLY.setVisibility(View.VISIBLE) + } } - } else if (pendingResult != null) { - // pendingResult.success(null); - clearMethodCallAndResult(); - return true; } + + +// if (resultCode == Activity.RESULT_OK) { +// if (requestCode == TrimmerActivity.REQUEST_VIDEO_TRIMMER) { +// var data = data?.getStringExtra(TrimmerActivity.EXTRA_OUTPUT_FILE); +// if (data != null) { +// finishWithSuccess(data) +// } else { +// finishWithError("crop_error", "Output path null", Throwable(message = "Output path null")) +// } +// +// return true +// +// } +// } else if (pendingResult != null) { +// // pendingResult.success(null); +// clearMethodCallAndResult(); +// return true; +// } return false } diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimmerActivity.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimmerActivity.java new file mode 100644 index 0000000..616801e --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/VideoTrimmerActivity.java @@ -0,0 +1,416 @@ +package com.steelkiwi.videotrimming; + +import android.app.ProgressDialog; +import android.content.Intent; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.VideoView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.steelkiwi.videotrimming.view.Utility; +import com.steelkiwi.videotrimming.view.customVideoViews.BackgroundTask; +import com.steelkiwi.videotrimming.view.customVideoViews.BarThumb; +import com.steelkiwi.videotrimming.view.customVideoViews.CustomRangeSeekBar; +import com.steelkiwi.videotrimming.view.customVideoViews.OnRangeSeekBarChangeListener; +import com.steelkiwi.videotrimming.view.customVideoViews.OnVideoTrimListener; +import com.steelkiwi.videotrimming.view.customVideoViews.TileView; + +import java.io.File; +import java.util.Date; +import java.util.Objects; + + +public class VideoTrimmerActivity extends AppCompatActivity implements View.OnClickListener { + + private TextView txtVideoCancel; + private TextView txtVideoUpload; + private TextView txtVideoEditTitle; + private RelativeLayout rlVideoView; + private TileView tileView; + private CustomRangeSeekBar mCustomRangeSeekBarNew; + private VideoView mVideoView; + private ImageView imgPlay; + private SeekBar seekBarVideo; + private TextView txtVideoLength; + + private int mDuration = 0; + private int mTimeVideo = 0; + private int mStartPosition = 0; + private int mEndPosition = 0; + // set your max video trim seconds + private final int mMaxDuration = 15; + private Handler mHandler = new Handler(); + + private ProgressDialog mProgressDialog; + String srcFile; + String dstFile; + + OnVideoTrimListener mOnVideoTrimListener = new OnVideoTrimListener() { + @Override + public void onTrimStarted() { + // Create an indeterminate progress dialog + + mProgressDialog = new ProgressDialog(VideoTrimmerActivity.this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setTitle(getString(R.string.save)); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + + } + + @Override + public void getResult(Uri uri) { + Log.d("getResult", "getResult: " + uri); + // mProgressDialog.dismiss(); + Bundle conData = new Bundle(); + conData.putString("INTENT_VIDEO_FILE", uri.getPath()); + Intent intent = new Intent(); + intent.putExtras(conData); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void cancelAction() { + mProgressDialog.dismiss(); + } + + @Override + public void onError(String message) { + mProgressDialog.dismiss(); + } + }; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_video_trim); + + txtVideoCancel = findViewById(R.id.txtVideoCancel); + txtVideoUpload = findViewById(R.id.txtVideoUpload); +// txtVideoEditTitle = (TextView) findViewById(R.id.txtVideoEditTitle); + tileView = findViewById(R.id.timeLineView); + mCustomRangeSeekBarNew = findViewById(R.id.timeLineBar); + mVideoView = findViewById(R.id.videoView); + imgPlay = findViewById(R.id.imgPlay); + seekBarVideo = findViewById(R.id.seekBarVideo); + txtVideoLength = findViewById(R.id.txtVideoLength); + + if (getIntent().getExtras() != null) { + srcFile = getIntent().getExtras().getString("EXTRA_PATH"); + } + dstFile = new Date().getTime() + Utility.VIDEO_FORMAT; + + tileView.post(() -> { + setBitmap(Uri.parse(srcFile)); + mVideoView.setVideoURI(Uri.parse(srcFile)); + }); + + txtVideoCancel.setOnClickListener(this); + txtVideoUpload.setOnClickListener(this); + + mVideoView.setOnPreparedListener(this::onVideoPrepared); + + mVideoView.setOnCompletionListener(mp -> onVideoCompleted()); + + // handle your range seekbar changes + mCustomRangeSeekBarNew.addOnRangeSeekBarListener(new OnRangeSeekBarChangeListener() { + @Override + public void onCreate(CustomRangeSeekBar customRangeSeekBarNew, int index, float value) { + // Do nothing + } + + @Override + public void onSeek(CustomRangeSeekBar customRangeSeekBarNew, int index, float value) { + onSeekThumbs(index, value); + } + + @Override + public void onSeekStart(CustomRangeSeekBar customRangeSeekBarNew, int index, float value) { + if (mVideoView != null) { + mHandler.removeCallbacks(mUpdateTimeTask); + seekBarVideo.setProgress(0); + mVideoView.seekTo(mStartPosition * 1000); + mVideoView.pause(); + imgPlay.setBackgroundResource(R.drawable.ic_white_play); + } + } + + @Override + public void onSeekStop(CustomRangeSeekBar customRangeSeekBarNew, int index, float value) { + onStopSeekThumbs(); + } + }); + + imgPlay.setOnClickListener(this); + + // handle changes on seekbar for video play + seekBarVideo.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (mVideoView != null) { + mHandler.removeCallbacks(mUpdateTimeTask); + seekBarVideo.setMax(mTimeVideo * 1000); + seekBarVideo.setProgress(0); + mVideoView.seekTo(mStartPosition * 1000); + mVideoView.pause(); + imgPlay.setBackgroundResource(R.drawable.ic_white_play); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mHandler.removeCallbacks(mUpdateTimeTask); + mVideoView.seekTo((mStartPosition * 1000) - seekBarVideo.getProgress()); + } + }); + } + + @Override + public void onClick(View view) { + if (view == txtVideoCancel) { + finish(); + } else if (view == txtVideoUpload) { + int diff = mEndPosition - mStartPosition; + if (diff < 3) { + Toast.makeText(VideoTrimmerActivity.this, getString(R.string.video_length_validation), + Toast.LENGTH_LONG).show(); + } else { + MediaMetadataRetriever + mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(VideoTrimmerActivity.this, Uri.parse(srcFile)); + final File file = new File(srcFile); + + //notify that video trimming started + if (mOnVideoTrimListener != null) + mOnVideoTrimListener.onTrimStarted(); + + BackgroundTask.execute(new BackgroundTask.Task("", 5L, "") { + @Override + public void execute() { + try { + Utility.startTrim(file, dstFile, mStartPosition * 1000, mEndPosition * 1000, mOnVideoTrimListener,getApplicationContext()); + } catch (final Throwable e) { + Objects.requireNonNull(Thread.getDefaultUncaughtExceptionHandler()).uncaughtException(Thread.currentThread(), e); + } + } + } + ); + + + } + + } else if (view == imgPlay) { + if (mVideoView.isPlaying()) { + if (mVideoView != null) { + mVideoView.pause(); + imgPlay.setBackgroundResource(R.drawable.ic_white_play); + } + } else { + if (mVideoView != null) { + mVideoView.start(); + imgPlay.setBackgroundResource(R.drawable.ic_white_pause); + if (seekBarVideo.getProgress() == 0) { + txtVideoLength.setText("00:00"); + updateProgressBar(); + } + } + } + } + } + + private void setBitmap(Uri mVideoUri) { + tileView.setVideo(mVideoUri); + } + + private void onVideoPrepared(@NonNull MediaPlayer mp) { + // Adjust the size of the video + // so it fits on the screen + //TODO manage proportion for video + /*int videoWidth = mp.getVideoWidth(); + int videoHeight = mp.getVideoHeight(); + float videoProportion = (float) videoWidth / (float) videoHeight; + int screenWidth = rlVideoView.getWidth(); + int screenHeight = rlVideoView.getHeight(); + float screenProportion = (float) screenWidth / (float) screenHeight; + ViewGroup.LayoutParams lp = mVideoView.getLayoutParams(); + + if (videoProportion > screenProportion) { + lp.width = screenWidth; + lp.height = (int) ((float) screenWidth / videoProportion); + } else { + lp.width = (int) (videoProportion * (float) screenHeight); + lp.height = screenHeight; + } + mVideoView.setLayoutParams(lp);*/ + + mDuration = mVideoView.getDuration() / 1000; + setSeekBarPosition(); + } + + public void updateProgressBar() { + mHandler.postDelayed(mUpdateTimeTask, 100); + } + + private Runnable mUpdateTimeTask = new Runnable() { + public void run() { + if (seekBarVideo.getProgress() >= seekBarVideo.getMax()) { + seekBarVideo.setProgress((mVideoView.getCurrentPosition() - mStartPosition * 1000)); + txtVideoLength.setText(milliSecondsToTimer(seekBarVideo.getProgress()) + ""); + mVideoView.seekTo(mStartPosition * 1000); + mVideoView.pause(); + seekBarVideo.setProgress(0); + txtVideoLength.setText("00:00"); + imgPlay.setBackgroundResource(R.drawable.ic_white_play); + } else { + seekBarVideo.setProgress((mVideoView.getCurrentPosition() - mStartPosition * 1000)); + txtVideoLength.setText(milliSecondsToTimer(seekBarVideo.getProgress()) + ""); + mHandler.postDelayed(this, 100); + } + } + }; + + private void setSeekBarPosition() { + + if (mDuration >= mMaxDuration) { + mStartPosition = 0; + mEndPosition = mMaxDuration; + + mCustomRangeSeekBarNew.setThumbValue(0, (mStartPosition * 100) / mDuration); + mCustomRangeSeekBarNew.setThumbValue(1, (mEndPosition * 100) / mDuration); + + } else { + mStartPosition = 0; + mEndPosition = mDuration; + } + + + mTimeVideo = mDuration; + mCustomRangeSeekBarNew.initMaxWidth(); + seekBarVideo.setMax(mMaxDuration * 1000); + mVideoView.seekTo(mStartPosition * 1000); + + String mStart = mStartPosition + ""; + if (mStartPosition < 10) + mStart = "0" + mStartPosition; + + int startMin = Integer.parseInt(mStart) / 60; + int startSec = Integer.parseInt(mStart) % 60; + + String mEnd = mEndPosition + ""; + if (mEndPosition < 10) + mEnd = "0" + mEndPosition; + + int endMin = Integer.parseInt(mEnd) / 60; + int endSec = Integer.parseInt(mEnd) % 60; + + } + + /** + * called when playing video completes + */ + private void onVideoCompleted() { + mHandler.removeCallbacks(mUpdateTimeTask); + seekBarVideo.setProgress(0); + mVideoView.seekTo(mStartPosition * 1000); + mVideoView.pause(); + imgPlay.setBackgroundResource(R.drawable.ic_white_play); + } + + /** + * Handle changes of left and right thumb movements + * + * @param index index of thumb + * @param value value + */ + private void onSeekThumbs(int index, float value) { + switch (index) { + case BarThumb.LEFT: { + mStartPosition = (int) ((mDuration * value) / 100L); + mVideoView.seekTo(mStartPosition * 1000); + break; + } + case BarThumb.RIGHT: { + mEndPosition = (int) ((mDuration * value) / 100L); + break; + } + } + mTimeVideo = (mEndPosition - mStartPosition); + seekBarVideo.setMax(mTimeVideo * 1000); + seekBarVideo.setProgress(0); + mVideoView.seekTo(mStartPosition * 1000); + + String mStart = mStartPosition + ""; + if (mStartPosition < 10) + mStart = "0" + mStartPosition; + + int startMin = Integer.parseInt(mStart) / 60; + int startSec = Integer.parseInt(mStart) % 60; + + String mEnd = mEndPosition + ""; + if (mEndPosition < 10) + mEnd = "0" + mEndPosition; + int endMin = Integer.parseInt(mEnd) / 60; + int endSec = Integer.parseInt(mEnd) % 60; + + + } + + private void onStopSeekThumbs() { +// mMessageHandler.removeMessages(SHOW_PROGRESS); +// mVideoView.pause(); +// mPlayView.setVisibility(View.VISIBLE); + } + + public String milliSecondsToTimer(long milliseconds) { + String finalTimerString = ""; + String secondsString; + String minutesString; + + + int hours = (int) (milliseconds / (1000 * 60 * 60)); + int minutes = (int) (milliseconds % (1000 * 60 * 60)) / (1000 * 60); + int seconds = (int) ((milliseconds % (1000 * 60 * 60)) % (1000 * 60) / 1000); + // Add hours if there + if (hours > 0) { + finalTimerString = hours + ":"; + } + + // Prepending 0 to seconds if it is one digit + if (seconds < 10) { + secondsString = "0" + seconds; + } else { + secondsString = "" + seconds; + } + + if (minutes < 10) { + minutesString = "0" + minutes; + } else { + minutesString = "" + minutes; + } + + finalTimerString = finalTimerString + minutesString + ":" + secondsString; + + // return timer string + return finalTimerString; + } +} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/VideotrimmingPlugin.kt b/android/src/main/kotlin/com/steelkiwi/videotrimming/VideotrimmingPlugin.kt index 6f93080..fe00038 100644 --- a/android/src/main/kotlin/com/steelkiwi/videotrimming/VideotrimmingPlugin.kt +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/VideotrimmingPlugin.kt @@ -27,7 +27,6 @@ public class VideotrimmingPlugin : FlutterPlugin, MethodCallHandler, ActivityAwa private val ACTION_CHANEL_TRIM_VIDEO = "trim_video" @JvmStatic fun registerWith(registrar: Registrar) { - val plugin = VideotrimmingPlugin() plugin.setupEngine(registrar.messenger()) val delegate: VideoTrimDelegate = plugin.setupActivity(registrar.activity())!! diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/ThirdPartyIntentsUtil.kt b/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/ThirdPartyIntentsUtil.kt deleted file mode 100644 index 5f07ec2..0000000 --- a/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/ThirdPartyIntentsUtil.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.steelkiwi.videotrimming.trim - -import android.annotation.TargetApi -import android.content.ComponentName -import android.content.ContentResolver -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Parcelable -import android.provider.MediaStore -import android.webkit.MimeTypeMap -import androidx.annotation.RequiresApi -import java.util.* - -object ThirdPartyIntentsUtil { - // https://medium.com/@louis993546/how-to-ask-system-to-open-intent-to-select-jpg-and-png-only-on-android-i-e-no-gif-e0491af240bf - //example usage: mainType= "*/*" extraMimeTypes= arrayOf("image/*", "video/*") - choose all images and videos - //example usage: mainType= "image/*" extraMimeTypes= arrayOf("image/jpeg", "image/png") - choose all images of png and jpeg types - /**note that this only requests to choose the files, but it's not guaranteed that this is what you will get*/ - @JvmStatic - fun getPickFileIntent(context: Context, mainType: String = "*/*", extraMimeTypes: Array = arrayOf()): Intent? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) - return null - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = mainType - if (extraMimeTypes.isNotEmpty()) - intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes) - if (context.packageManager.queryIntentActivities(intent, 0).isEmpty()) - return null - return intent - } - - // https://github.com/linchaolong/ImagePicker/blob/master/library/src/main/java/com/linchaolong/android/imagepicker/cropper/CropImage.java - @RequiresApi(Build.VERSION_CODES.DONUT) - @JvmStatic - fun getPickFileChooserIntent( - context: Context, title: CharSequence?, preferDocuments: Boolean = true, includeCameraIntents: Boolean, mainType: String - , extraMimeTypes: Array? = null, extraIntents: ArrayList? = null - ): Intent? { - val packageManager = context.packageManager - var allIntents = - getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT, mainType, extraMimeTypes) - if (allIntents.isEmpty()) { - // if no intents found for get-content try pick intent action (Huawei P9). - allIntents = - getGalleryIntents(packageManager, Intent.ACTION_PICK, mainType, extraMimeTypes) - } - if (includeCameraIntents) { - val cameraIntents = getCameraIntents(packageManager) - allIntents.addAll(0, cameraIntents) - } - // Log.d("AppLog", "got ${allIntents.size} intents") - if (allIntents.isEmpty()) - return null - if (preferDocuments) - for (intent in allIntents) - if (intent.component!!.packageName == "com.android.documentsui") - return intent - if (allIntents.size == 1) - return allIntents[0] - var target: Intent? = null - for ((index, intent) in allIntents.withIndex()) { - if (intent.component!!.packageName == "com.android.documentsui") { - target = intent - allIntents.removeAt(index) - break - } - } - if (target == null) - target = allIntents[allIntents.size - 1] - allIntents.removeAt(allIntents.size - 1) - // Create a chooser from the main intent - val chooserIntent = Intent.createChooser(target, title) - if (extraIntents != null && extraIntents.isNotEmpty()) - allIntents.addAll(extraIntents) - // Add all other intents - chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toTypedArray()) - return chooserIntent - } - - @RequiresApi(Build.VERSION_CODES.DONUT) - private fun getCameraIntents(packageManager: PackageManager): ArrayList { - val cameraIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) - val listCamera = packageManager.queryIntentActivities(cameraIntent, 0) - val intents = ArrayList() - for (res in listCamera) { - val intent = Intent(cameraIntent) - intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name) - intent.`package` = res.activityInfo.packageName - intents.add(intent) - } - return intents - } - - /** - * Get all Gallery intents for getting image from one of the apps of the device that handle - * images. Intent.ACTION_GET_CONTENT and then Intent.ACTION_PICK - */ - @TargetApi(Build.VERSION_CODES.KITKAT) - private fun getGalleryIntents( - packageManager: PackageManager, action: String, - mainType: String, extraMimeTypes: Array? = null - ): ArrayList { - val galleryIntent = if (action == Intent.ACTION_GET_CONTENT) - Intent(action) - else - Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) - galleryIntent.type = mainType - if (!extraMimeTypes!!.isEmpty()) { - galleryIntent.addCategory(Intent.CATEGORY_OPENABLE) - galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes) - } - val listGallery = packageManager.queryIntentActivities(galleryIntent, 0) - val intents = ArrayList() - for (res in listGallery) { - val intent = Intent(galleryIntent) - intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name) - intent.`package` = res.activityInfo.packageName - intents.add(intent) - } - return intents - } - - @JvmStatic - fun getMimeType(context: Context, uri: Uri): String? { - return if (ContentResolver.SCHEME_CONTENT == uri.scheme) { - val cr = context.contentResolver - cr.getType(uri) - } else { - val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()) - MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()) - } - } - -} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/TrimmerActivity.kt b/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/TrimmerActivity.kt deleted file mode 100644 index ec96f27..0000000 --- a/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/TrimmerActivity.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.steelkiwi.videotrimming.trim - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.annotation.RequiresApi -import com.lb.video_trimmer_library.interfaces.VideoTrimmingListener -import com.steelkiwi.videotrimming.R -import io.flutter.app.FlutterActivity -import kotlinx.android.synthetic.main.activity_trimmer.* -import java.io.File -import java.util.* - -class TrimmerActivity : FlutterActivity(), VideoTrimmingListener { -// private var progressDialog: ProgressDialog? = null - - - companion object { - const val REQUEST_VIDEO_TRIMMER = 1 - internal const val EXTRA_INPUT_URI = "EXTRA_INPUT_URI" - internal const val EXTRA_INPUT_MAX_SECONDS = "EXTRA_INPUT_MAX_SECONDS" - internal const val EXTRA_OUTPUT_FILE = "EXTRA_OUTPUT_FILE" -// private val allowedVideoFileExtensions = arrayOf("mkv", "mp4", "3gp", "mov", "mts") -// private val videosMimeTypes = ArrayList(allowedVideoFileExtensions.size) - } - - @RequiresApi(Build.VERSION_CODES.FROYO) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_trimmer) - var videoTrimingView=findViewById(R.id.videoTrimmerView); - videoTrimingView.onCancelListener={ - setResult(Activity.RESULT_CANCELED, intent) - finish() - }; - val inputVideoUri: String? = intent?.getStringExtra(EXTRA_INPUT_URI) - val maxSeconds: Double? = intent?.getDoubleExtra(EXTRA_INPUT_MAX_SECONDS, 15.0) ?: 15.0; - if (inputVideoUri == null) { - finish() - return - } - //setting progressbar - if (maxSeconds != null) { - videoTrimmerView.setMaxDurationInMs((maxSeconds * 1000).toInt()) - } - - videoTrimmerView.setOnK4LVideoListener(this) - val parentFolder = getExternalFilesDir(null)!! - parentFolder.mkdirs() - val fileName = "trimmedVideo_${System.currentTimeMillis()}.mp4" - val trimmedVideoFile = File(parentFolder, fileName) - videoTrimmerView.setDestinationFile(trimmedVideoFile) - videoTrimmerView.setVideoURI(Uri.fromFile(File(inputVideoUri))) - - videoTrimmerView.setVideoInformationVisibility(true) - } - - override fun onTrimStarted() { - trimmingProgressView.visibility = View.VISIBLE - } - - override fun onFinishedTrimming(uri: Uri?) { - trimmingProgressView.visibility = View.GONE - if (uri == null) { - Toast.makeText(this@TrimmerActivity, "failed trimming", Toast.LENGTH_SHORT).show() - } else { - val msg = "saved " + uri.path; - Toast.makeText(this@TrimmerActivity, msg, Toast.LENGTH_SHORT).show() - val intent = Intent() - intent.putExtra(EXTRA_OUTPUT_FILE, uri.path) - setResult(Activity.RESULT_OK, intent) - finish() -// val intent = Intent(Intent.ACTION_VIEW, uri) -// intent.setDataAndType(uri, "video/mp4") -// startActivity(intent) - } - //finish() - } - - override fun onErrorWhileViewingVideo(what: Int, extra: Int) { - trimmingProgressView.visibility = View.GONE - Toast.makeText(this@TrimmerActivity, "error while previewing video", Toast.LENGTH_SHORT).show() - } - - override fun onVideoPrepared() { - // Toast.makeText(TrimmerActivity.this, "onVideoPrepared", Toast.LENGTH_SHORT).show(); - } -} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/VideoTrimmerView.kt b/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/VideoTrimmerView.kt deleted file mode 100644 index 8d3805b..0000000 --- a/android/src/main/kotlin/com/steelkiwi/videotrimming/trim/VideoTrimmerView.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.steelkiwi.videotrimming.trim - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.VideoView -import com.lb.video_trimmer_library.BaseVideoTrimmerView -import com.lb.video_trimmer_library.view.RangeSeekBarView -import com.lb.video_trimmer_library.view.TimeLineView - -import com.steelkiwi.videotrimming.R -import kotlinx.android.synthetic.main.video_trimmer.view.* - - -class VideoTrimmerView @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : BaseVideoTrimmerView(context, attrs, defStyleAttr) { - var onCancelListener: () -> Unit = fun() {} - - - private fun stringForTime(timeMs: Int): String { - val totalSeconds = timeMs / 1000 - val seconds = totalSeconds % 60 - val minutes = totalSeconds / 60 % 60 - val hours = totalSeconds / 3600 - val timeFormatter = java.util.Formatter() - return if (hours > 0) - timeFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - else - timeFormatter.format("%02d:%02d", minutes, seconds).toString() - } - - override fun initRootView() { - LayoutInflater.from(context).inflate(R.layout.video_trimmer, this, true) - fab.setOnClickListener { initiateTrimming() } - cancel.setOnClickListener { onCancelListener() } - } - - override fun getTimeLineView(): TimeLineView = timeLineView - - override fun getTimeInfoContainer(): View = timeTextContainer - - override fun getPlayView(): View = playIndicatorView - - override fun getVideoView(): VideoView = videoView - - override fun getVideoViewContainer(): View = videoViewContainer - - override fun getRangeSeekBarView(): RangeSeekBarView = rangeSeekBarView - - override fun onRangeUpdated(startTimeInMs: Int, endTimeInMs: Int) { - val seconds = "s" - trimTimeRangeTextView.text = "${stringForTime(startTimeInMs)} $seconds - ${stringForTime(endTimeInMs)} $seconds" - } - - override fun onVideoPlaybackReachingTime(timeInMs: Int) { - val seconds = "s" - playbackTimeTextView.text = "${stringForTime(timeInMs)} $seconds" - } - - override fun onGotVideoFileSize(videoFileSize: Long) { - //videoFileSizeTextView.text = Formatter.formatShortFileSize(context, videoFileSize) - } -} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/Utility.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/Utility.java new file mode 100644 index 0000000..799e1a3 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/Utility.java @@ -0,0 +1,123 @@ +package com.steelkiwi.videotrimming.view; + +import android.content.Context; +import android.content.ContextWrapper; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.coremedia.iso.boxes.Container; +import com.googlecode.mp4parser.FileDataSourceViaHeapImpl; +import com.googlecode.mp4parser.authoring.Movie; +import com.googlecode.mp4parser.authoring.Track; +import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder; +import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; +import com.googlecode.mp4parser.authoring.tracks.AppendTrack; +import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; +import com.steelkiwi.videotrimming.view.customVideoViews.OnVideoTrimListener; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +public class Utility { + public static final String VIDEO_FORMAT = ".mp4"; + private static final String TAG = Utility.class.getSimpleName(); + + public static void startTrim(@NonNull File src, @NonNull String dst, long startMs, long endMs, + @NonNull OnVideoTrimListener callback, Context context) throws IOException { + final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + ContextWrapper cw = new ContextWrapper(context); + File directory = cw.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS); + File file = new File(directory, dst); + file.getParentFile().mkdirs(); + Log.d(TAG, "Generated file path " + file.getPath()); + generateVideo(src, file, startMs, endMs, callback); + + + } + + private static void generateVideo(@NonNull File src, @NonNull File dst, long startMs, + long endMs, @NonNull OnVideoTrimListener callback) throws IOException { + + + // NOTE: Switched to using FileDataSourceViaHeapImpl since it does not use memory mapping (VM). + // Otherwise we get OOM with large movie files. + Movie movie = MovieCreator.build(new FileDataSourceViaHeapImpl(src.getAbsolutePath())); + + List tracks = movie.getTracks(); + movie.setTracks(new LinkedList()); + // remove all tracks we will create new tracks from the old + + double startTime1 = startMs / 1000; + double endTime1 = endMs / 1000; + + boolean timeCorrected = false; + + // Here we try to find a track that has sync samples. Since we can only start decoding + // at such a sample we SHOULD make sure that the start of the new fragment is exactly + // such a frame + for (Track track : tracks) { + if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { + if (timeCorrected) { + // This exception here could be a false positive in case we have multiple tracks + // with sync samples at exactly the same positions. E.g. a single movie containing + // multiple qualities of the same video (Microsoft Smooth Streaming file) + + throw new RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported."); + } +// startTime1 = correctTimeToSyncSample(track, startTime1, false); +// endTime1 = correctTimeToSyncSample(track, endTime1, true); + timeCorrected = true; + } + } + + for (Track track : tracks) { + long currentSample = 0; + double currentTime = 0; + double lastTime = -1; + long startSample1 = -1; + long endSample1 = -1; + + for (int i = 0; i < track.getSampleDurations().length; i++) { + long delta = track.getSampleDurations()[i]; + + + if (currentTime > lastTime && currentTime <= startTime1) { + // current sample is still before the new starttime + startSample1 = currentSample; + } + if (currentTime > lastTime && currentTime <= endTime1) { + // current sample is after the new start time and still before the new endtime + endSample1 = currentSample; + } + lastTime = currentTime; + currentTime += (double) delta / (double) track.getTrackMetaData().getTimescale(); + currentSample++; + } + movie.addTrack(new AppendTrack(new CroppedTrack(track, startSample1, endSample1))); + } + + Container out = new DefaultMp4Builder().build(movie); + + FileOutputStream fos = new FileOutputStream(dst); + FileChannel fc = fos.getChannel(); + out.writeContainer(fc); + + fc.close(); + fos.close(); + if (callback != null) + callback.getResult(Uri.parse(dst.toString())); + + } + + +} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BackgroundTask.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BackgroundTask.java new file mode 100644 index 0000000..c548336 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BackgroundTask.java @@ -0,0 +1,233 @@ +package com.steelkiwi.videotrimming.view.customVideoViews; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class BackgroundTask { + + private static final String TAG = "BackgroundTask"; + + public static final Executor DEFAULT_EXECUTOR = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors()); + private static Executor executor = DEFAULT_EXECUTOR; + private static final List TASKS = new ArrayList<>(); + private static final ThreadLocal CURRENT_SERIAL = new ThreadLocal<>(); + + private BackgroundTask() { + } + + /** + * Execute a runnable after the given delay. + * + * @param runnable the task to execute + * @param delay the time from now to delay execution, in milliseconds + *

+ * if delay is strictly positive and the current + * executor does not support scheduling (if + * Executor has been called with such an + * executor) + * @return Future associated to the running task + * @throws IllegalArgumentException if the current executor set by Executor + * does not support scheduling + */ + private static Future directExecute(Runnable runnable, long delay) { + Future future = null; + if (delay > 0) { + /* no serial, but a delay: schedule the task */ + if (!(executor instanceof ScheduledExecutorService)) { + throw new IllegalArgumentException("The executor set does not support scheduling"); + } + ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor; + future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS); + } else { + if (executor instanceof ExecutorService) { + ExecutorService executorService = (ExecutorService) executor; + future = executorService.submit(runnable); + } else { + /* non-cancellable task */ + executor.execute(runnable); + } + } + return future; + } + + /** + * Execute a task after (at least) its delay and after all + * tasks added with the same non-null serial (if any) have + * completed execution. + * + * @param task the task to execute + * @throws IllegalArgumentException if task.delay is strictly positive and the + * current executor does not support scheduling (if + * Executor has been called with such an + * executor) + */ + public static synchronized void execute(Task task) { + Future future = null; + if (task.serial == null || !hasRunning(task.serial)) { + task.executionAsked = true; + future = directExecute(task, task.remainingDelay); + } + if ((task.id != null || task.serial != null) && !task.managed.get()) { + /* keep task */ + task.future = future; + TASKS.add(task); + } + } + + /** + * Indicates whether a task with the specified serial has been + * submitted to the executor. + * + * @param serial the serial queue + * @return true if such a task has been submitted, + * false otherwise + */ + private static boolean hasRunning(String serial) { + for (Task task : TASKS) { + if (task.executionAsked && serial.equals(task.serial)) { + return true; + } + } + return false; + } + + /** + * Retrieve and remove the first task having the specified + * serial (if any). + * + * @param serial the serial queue + * @return task if found, null otherwise + */ + private static Task take(String serial) { + int len = TASKS.size(); + for (int i = 0; i < len; i++) { + if (serial.equals(TASKS.get(i).serial)) { + return TASKS.remove(i); + } + } + return null; + } + + /** + * Cancel all tasks having the specified id. + * + * @param id the cancellation identifier + * @param mayInterruptIfRunning true if the thread executing this task should be + * interrupted; otherwise, in-progress tasks are allowed to + * complete + */ + public static synchronized void cancelAllTask(String id, boolean mayInterruptIfRunning) { + for (int i = TASKS.size() - 1; i >= 0; i--) { + Task task = TASKS.get(i); + if (id.equals(task.id)) { + if (task.future != null) { + task.future.cancel(mayInterruptIfRunning); + if (!task.managed.getAndSet(true)) { + /* + * the task has been submitted to the executor, but its + * execution has not started yet, so that its run() + * method will never call postExecute() + */ + task.postExecute(); + } + } else if (task.executionAsked) { + Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)"); + } else { + /* this task has not been submitted to the executor */ + TASKS.remove(i); + } + } + } + } + + public static abstract class Task implements Runnable { + + private String id; + private long remainingDelay; + private long targetTimeMillis; /* since epoch */ + private String serial; + private boolean executionAsked; + private Future future; + + /* + * A task can be cancelled after it has been submitted to the executor + * but before its run() method is called. In that case, run() will never + * be called, hence neither will postExecute(): the tasks with the same + * serial identifier (if any) will never be submitted. + * + * Therefore, cancelAllTask() *must* call postExecute() if run() is not + * started. + * + * This flag guarantees that either cancelAllTask() or run() manages this + * task post execution, but not both. + */ + private AtomicBoolean managed = new AtomicBoolean(); + + protected Task(String id, long delay, String serial) { + if (!"".equals(id)) { + this.id = id; + } + if (delay > 0) { + remainingDelay = delay; + targetTimeMillis = System.currentTimeMillis() + delay; + } + if (!"".equals(serial)) { + this.serial = serial; + } + } + + @Override + public void run() { + if (managed.getAndSet(true)) { + /* cancelled and postExecute() already called */ + return; + } + + try { + CURRENT_SERIAL.set(serial); + execute(); + } finally { + /* handle next tasks */ + postExecute(); + } + } + + public abstract void execute(); + + private void postExecute() { + if (id == null && serial == null) { + /* nothing to do */ + return; + } + CURRENT_SERIAL.set(null); + synchronized (BackgroundTask.class) { + /* execution complete */ + TASKS.remove(this); + + if (serial != null) { + Task next = take(serial); + if (next != null) { + if (next.remainingDelay != 0) { + /* the delay may not have elapsed yet */ + next.remainingDelay = Math.max(0L, targetTimeMillis - System.currentTimeMillis()); + } + /* a task having the same serial was queued, execute it */ + BackgroundTask.execute(next); + } + } + } + } + } + + +} + diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BarThumb.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BarThumb.java new file mode 100644 index 0000000..ed0fab9 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/BarThumb.java @@ -0,0 +1,113 @@ +package com.steelkiwi.videotrimming.view.customVideoViews; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import androidx.annotation.NonNull; + +import com.steelkiwi.videotrimming.R; + +import java.util.List; +import java.util.Vector; + + +public class BarThumb { + + public static final int LEFT = 0; + public static final int RIGHT = 1; + + private int mIndex; + private float mVal; + private float mPos; + private Bitmap mBitmap; + private int mWidthBitmap; + private int mHeightBitmap; + + private float mLastTouchX; + + private BarThumb() { + mVal = 0; + mPos = 0; + } + + public int getIndex() { + return mIndex; + } + + private void setIndex(int index) { + mIndex = index; + } + + public float getVal() { + return mVal; + } + + public void setVal(float val) { + mVal = val; + } + + public float getPos() { + return mPos; + } + + public void setPos(float pos) { + mPos = pos; + } + + public Bitmap getBitmap() { + return mBitmap; + } + + private void setBitmap(@NonNull Bitmap bitmap) { + mBitmap = bitmap; + mWidthBitmap = bitmap.getWidth(); + mHeightBitmap = bitmap.getHeight(); + } + + @NonNull + public static List initThumbs(Resources resources) { + + List barThumbs = new Vector<>(); + + for (int i = 0; i < 2; i++) { + BarThumb th = new BarThumb(); + th.setIndex(i); + if (i == 0) { + int resImageLeft = R.drawable.time_line_a; + th.setBitmap(BitmapFactory.decodeResource(resources, resImageLeft)); + } else { + int resImageRight = R.drawable.time_line_a; + th.setBitmap(BitmapFactory.decodeResource(resources, resImageRight)); + } + + barThumbs.add(th); + } + + return barThumbs; + } + + public static int getWidthBitmap(@NonNull List barThumbs) { + return barThumbs.get(0).getWidthBitmap(); + } + + public static int getHeightBitmap(@NonNull List barThumbs) { + return barThumbs.get(0).getHeightBitmap(); + } + + public float getLastTouchX() { + return mLastTouchX; + } + + public void setLastTouchX(float lastTouchX) { + mLastTouchX = lastTouchX; + } + + public int getWidthBitmap() { + return mWidthBitmap; + } + + private int getHeightBitmap() { + return mHeightBitmap; + } +} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/CustomRangeSeekBar.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/CustomRangeSeekBar.java new file mode 100644 index 0000000..b715015 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/CustomRangeSeekBar.java @@ -0,0 +1,380 @@ +package com.steelkiwi.videotrimming.view.customVideoViews; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.steelkiwi.videotrimming.R; + +import java.util.ArrayList; +import java.util.List; + + +public class CustomRangeSeekBar extends View { + + private int mHeightTimeLine; + private List mBarThumbs; + private List mListeners; + private float mMaxWidth; + private float mThumbWidth; + private float mThumbHeight; + private int mViewWidth; + private float mPixelRangeMin; + private float mPixelRangeMax; + private float mScaleRangeMax; + private boolean mFirstRun; + + private final Paint mShadow = new Paint(); + private final Paint mLine = new Paint(); + + public CustomRangeSeekBar(@NonNull Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomRangeSeekBar(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mBarThumbs = BarThumb.initThumbs(getResources()); + mThumbWidth = BarThumb.getWidthBitmap(mBarThumbs); + mThumbHeight = BarThumb.getHeightBitmap(mBarThumbs); + + mScaleRangeMax = 100; + mHeightTimeLine = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height); + + setFocusable(true); + setFocusableInTouchMode(true); + + mFirstRun = true; + + int shadowColor = ContextCompat.getColor(getContext(), R.color.shadow_color); + mShadow.setAntiAlias(true); + mShadow.setColor(shadowColor); + mShadow.setAlpha(177); + + int lineColor = ContextCompat.getColor(getContext(), R.color.line_color); + mLine.setAntiAlias(true); + mLine.setColor(lineColor); + mLine.setAlpha(200); + } + + public void initMaxWidth() { + mMaxWidth = mBarThumbs.get(1).getPos() - mBarThumbs.get(0).getPos(); + + onSeekStop(this, 0, mBarThumbs.get(0).getVal()); + onSeekStop(this, 1, mBarThumbs.get(1).getVal()); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); + mViewWidth = resolveSizeAndState(minW, widthMeasureSpec, 1); + + int minH = getPaddingBottom() + getPaddingTop() + (int) mThumbHeight; + int viewHeight = resolveSizeAndState(minH, heightMeasureSpec, 1); + + setMeasuredDimension(mViewWidth, viewHeight); + + mPixelRangeMin = 0; + mPixelRangeMax = mViewWidth - mThumbWidth; + + if (mFirstRun) { + for (int i = 0; i < mBarThumbs.size(); i++) { + BarThumb th = mBarThumbs.get(i); + th.setVal(mScaleRangeMax * i); + th.setPos(mPixelRangeMax * i); + } + // Fire listener callback + onCreate(this, currentThumb, getThumbValue(currentThumb)); + mFirstRun = false; + } + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + + drawShadow(canvas); + drawThumbs(canvas); + } + + private int currentThumb = 0; + + @Override + public boolean onTouchEvent(@NonNull MotionEvent ev) { + final BarThumb mBarThumb; + final BarThumb mBarThumb2; + final float coordinate = ev.getX(); + final int action = ev.getAction(); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + // Remember where we started + currentThumb = getClosestThumb(coordinate); + + if (currentThumb == -1) { + return false; + } + + mBarThumb = mBarThumbs.get(currentThumb); + mBarThumb.setLastTouchX(coordinate); + onSeekStart(this, currentThumb, mBarThumb.getVal()); + return true; + } + case MotionEvent.ACTION_UP: { + + if (currentThumb == -1) { + return false; + } + + mBarThumb = mBarThumbs.get(currentThumb); + onSeekStop(this, currentThumb, mBarThumb.getVal()); + return true; + } + + case MotionEvent.ACTION_MOVE: { + mBarThumb = mBarThumbs.get(currentThumb); + mBarThumb2 = mBarThumbs.get(currentThumb == 0 ? 1 : 0); + // Calculate the distance moved + final float dx = coordinate - mBarThumb.getLastTouchX(); + final float newX = mBarThumb.getPos() + dx; + + if (currentThumb == 0) { + + if ((newX + mBarThumb.getWidthBitmap()) >= mBarThumb2.getPos()) { + mBarThumb.setPos(mBarThumb2.getPos() - mBarThumb.getWidthBitmap()); + } else if (newX <= mPixelRangeMin) { + mBarThumb.setPos(mPixelRangeMin); + if ((mBarThumb2.getPos() - (mBarThumb.getPos() + dx)) > mMaxWidth) { + mBarThumb2.setPos(mBarThumb.getPos() + dx + mMaxWidth); + setThumbPos(1, mBarThumb2.getPos()); + } + } else { + //Check if thumb is not out of max width +// checkPositionThumb(mBarThumb, mBarThumb2, dx, true, coordinate); + if ((mBarThumb2.getPos() - (mBarThumb.getPos() + dx)) > mMaxWidth) { + mBarThumb2.setPos(mBarThumb.getPos() + dx + mMaxWidth); + setThumbPos(1, mBarThumb2.getPos()); + } + // Move the object + mBarThumb.setPos(mBarThumb.getPos() + dx); + + // Remember this touch position for the next move event + mBarThumb.setLastTouchX(coordinate); + } + + } else { + if (newX <= mBarThumb2.getPos() + mBarThumb2.getWidthBitmap()) { + mBarThumb.setPos(mBarThumb2.getPos() + mBarThumb.getWidthBitmap()); + } else if (newX >= mPixelRangeMax) { + mBarThumb.setPos(mPixelRangeMax); + if (((mBarThumb.getPos() + dx) - mBarThumb2.getPos()) > mMaxWidth) { + mBarThumb2.setPos(mBarThumb.getPos() + dx - mMaxWidth); + setThumbPos(0, mBarThumb2.getPos()); + } + } else { + //Check if thumb is not out of max width +// checkPositionThumb(mBarThumb2, mBarThumb, dx, false, coordinate); + if (((mBarThumb.getPos() + dx) - mBarThumb2.getPos()) > mMaxWidth) { + mBarThumb2.setPos(mBarThumb.getPos() + dx - mMaxWidth); + setThumbPos(0, mBarThumb2.getPos()); + } + // Move the object + mBarThumb.setPos(mBarThumb.getPos() + dx); + // Remember this touch position for the next move event + mBarThumb.setLastTouchX(coordinate); + } + } + + setThumbPos(currentThumb, mBarThumb.getPos()); + + // Invalidate to request a redraw + invalidate(); + return true; + } + } + return false; + } + + private void checkPositionThumb(@NonNull BarThumb mBarThumbLeft, @NonNull BarThumb mBarThumbRight, float dx, boolean isLeftMove, float coordinate) { + + if (isLeftMove && dx < 0) { + if ((mBarThumbRight.getPos() - (mBarThumbLeft.getPos() + dx)) > mMaxWidth) { + mBarThumbRight.setPos(mBarThumbLeft.getPos() + dx + mMaxWidth); + setThumbPos(1, mBarThumbRight.getPos()); + } + } else if (!isLeftMove && dx > 0) { + if (((mBarThumbRight.getPos() + dx) - mBarThumbLeft.getPos()) > mMaxWidth) { + mBarThumbLeft.setPos(mBarThumbRight.getPos() + dx - mMaxWidth); + setThumbPos(0, mBarThumbLeft.getPos()); + } + } + + } + + + private float pixelToScale(int index, float pixelValue) { + float scale = (pixelValue * 100) / mPixelRangeMax; + if (index == 0) { + float pxThumb = (scale * mThumbWidth) / 100; + return scale + (pxThumb * 100) / mPixelRangeMax; + } else { + float pxThumb = ((100 - scale) * mThumbWidth) / 100; + return scale - (pxThumb * 100) / mPixelRangeMax; + } + } + + private float scaleToPixel(int index, float scaleValue) { + float px = (scaleValue * mPixelRangeMax) / 100; + if (index == 0) { + float pxThumb = (scaleValue * mThumbWidth) / 100; + return px - pxThumb; + } else { + float pxThumb = ((100 - scaleValue) * mThumbWidth) / 100; + return px + pxThumb; + } + } + + private void calculateThumbValue(int index) { + if (index < mBarThumbs.size() && !mBarThumbs.isEmpty()) { + BarThumb th = mBarThumbs.get(index); + th.setVal(pixelToScale(index, th.getPos())); + onSeek(this, index, th.getVal()); + } + } + + private void calculateThumbPos(int index) { + if (index < mBarThumbs.size() && !mBarThumbs.isEmpty()) { + BarThumb th = mBarThumbs.get(index); + th.setPos(scaleToPixel(index, th.getVal())); + } + } + + private float getThumbValue(int index) { + return mBarThumbs.get(index).getVal(); + } + + public void setThumbValue(int index, float value) { + mBarThumbs.get(index).setVal(value); + calculateThumbPos(index); + // Tell the view we want a complete redraw + invalidate(); + } + + private void setThumbPos(int index, float pos) { + mBarThumbs.get(index).setPos(pos); + calculateThumbValue(index); + // Tell the view we want a complete redraw + invalidate(); + } + + private int getClosestThumb(float coordinate) { + int closest = -1; + if (!mBarThumbs.isEmpty()) { + for (int i = 0; i < mBarThumbs.size(); i++) { + // Find thumb closest to x coordinate + final float tcoordinate = mBarThumbs.get(i).getPos() + mThumbWidth; + if (coordinate >= mBarThumbs.get(i).getPos() && coordinate <= tcoordinate) { + closest = mBarThumbs.get(i).getIndex(); + } + } + } + return closest; + } + + private void drawShadow(@NonNull Canvas canvas) { + if (!mBarThumbs.isEmpty()) { + + for (BarThumb th : mBarThumbs) { + if (th.getIndex() == 0) { + final float x = th.getPos(); + if (x > mPixelRangeMin) { + Rect mRect = new Rect(0, (int) (mThumbHeight - mHeightTimeLine) / 2, + (int) (x + (mThumbWidth / 2)), mHeightTimeLine + (int) (mThumbHeight - mHeightTimeLine) / 2); + canvas.drawRect(mRect, mShadow); + } + } else { + final float x = th.getPos(); + if (x < mPixelRangeMax) { + Rect mRect = new Rect((int) (x + (mThumbWidth / 2)), (int) (mThumbHeight - mHeightTimeLine) / 2, + (mViewWidth), mHeightTimeLine + (int) (mThumbHeight - mHeightTimeLine) / 2); + canvas.drawRect(mRect, mShadow); + } + } + } + } + } + + private void drawThumbs(@NonNull Canvas canvas) { + + if (!mBarThumbs.isEmpty()) { + for (BarThumb th : mBarThumbs) { + if (th.getIndex() == 0) { + canvas.drawBitmap(th.getBitmap(), th.getPos() + getPaddingLeft(), getPaddingTop(), null); + } else { + canvas.drawBitmap(th.getBitmap(), th.getPos() - getPaddingRight(), getPaddingTop(), null); + } + } + } + } + + public void addOnRangeSeekBarListener(OnRangeSeekBarChangeListener listener) { + + if (mListeners == null) { + mListeners = new ArrayList<>(); + } + + mListeners.add(listener); + } + + private void onCreate(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) { + if (mListeners == null) + return; + + for (OnRangeSeekBarChangeListener item : mListeners) { + item.onCreate(CustomRangeSeekBar, index, value); + } + } + + private void onSeek(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) { + if (mListeners == null) + return; + + for (OnRangeSeekBarChangeListener item : mListeners) { + item.onSeek(CustomRangeSeekBar, index, value); + } + } + + private void onSeekStart(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) { + if (mListeners == null) + return; + + for (OnRangeSeekBarChangeListener item : mListeners) { + item.onSeekStart(CustomRangeSeekBar, index, value); + } + } + + private void onSeekStop(CustomRangeSeekBar CustomRangeSeekBar, int index, float value) { + if (mListeners == null) + return; + + for (OnRangeSeekBarChangeListener item : mListeners) { + item.onSeekStop(CustomRangeSeekBar, index, value); + } + } + + public List getThumbs() { + return mBarThumbs; + } +} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnRangeSeekBarChangeListener.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnRangeSeekBarChangeListener.java new file mode 100644 index 0000000..b0659f7 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnRangeSeekBarChangeListener.java @@ -0,0 +1,10 @@ +package com.steelkiwi.videotrimming.view.customVideoViews; +public interface OnRangeSeekBarChangeListener { + void onCreate(CustomRangeSeekBar CustomRangeSeekBar, int index, float value); + + void onSeek(CustomRangeSeekBar CustomRangeSeekBar, int index, float value); + + void onSeekStart(CustomRangeSeekBar CustomRangeSeekBar, int index, float value); + + void onSeekStop(CustomRangeSeekBar CustomRangeSeekBar, int index, float value); +} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnVideoTrimListener.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnVideoTrimListener.java new file mode 100644 index 0000000..683c106 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/OnVideoTrimListener.java @@ -0,0 +1,14 @@ +package com.steelkiwi.videotrimming.view.customVideoViews; + +import android.net.Uri; + +public interface OnVideoTrimListener { + + void onTrimStarted(); + + void getResult(final Uri uri); + + void cancelAction(); + + void onError(final String message); +} diff --git a/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/TileView.java b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/TileView.java new file mode 100644 index 0000000..b449970 --- /dev/null +++ b/android/src/main/kotlin/com/steelkiwi/videotrimming/view/customVideoViews/TileView.java @@ -0,0 +1,239 @@ +package com.steelkiwi.videotrimming.view.customVideoViews; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.LongSparseArray; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import com.steelkiwi.videotrimming.R; +import java.util.HashMap; +import java.util.Map; + +public class TileView extends View { + + private Uri mVideoUri; + private int mHeightView; + private LongSparseArray mBitmapList = null; + private int viewWidth = 0; + private int viewHeight = 0; + + public TileView(@NonNull Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TileView(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mHeightView = getContext().getResources().getDimensionPixelOffset(R.dimen.frames_video_height); + } + + @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int minW = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); + int w = resolveSizeAndState(minW, widthMeasureSpec, 1); + + final int minH = getPaddingBottom() + getPaddingTop() + mHeightView; + int h = resolveSizeAndState(minH, heightMeasureSpec, 1); + + setMeasuredDimension(w, h); + } + + @Override + protected void onSizeChanged(final int w, int h, final int oldW, int oldH) { + super.onSizeChanged(w, h, oldW, oldH); + viewWidth = w; + viewHeight = h; + if (w != oldW) { + if (mVideoUri != null) + getBitmap(); + } + } + + private void getBitmap() { + BackgroundTask + .execute(new BackgroundTask.Task("", 0L, "") { + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void execute() { + try { + LongSparseArray thumbnailList = new LongSparseArray<>(); + + MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(getContext(), mVideoUri); + + // Retrieve media data + long videoLengthInMs = Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000; + + // Set thumbnail properties (Thumbs are squares) + final int thumbWidth = mHeightView; + final int thumbHeight = mHeightView; + + int numThumbs = (int) Math.ceil(((float) viewWidth) / thumbWidth); + + final long interval = videoLengthInMs / numThumbs; + + for (int i = 0; i < numThumbs; ++i) { + Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); + // TODO: bitmap might be null here, hence throwing NullPointerException. You were right + try { + bitmap = Bitmap.createScaledBitmap(bitmap, thumbWidth, thumbHeight, false); + } catch (Exception e) { + e.printStackTrace(); + } + thumbnailList.put(i, bitmap); + } + + mediaMetadataRetriever.release(); + returnBitmaps(thumbnailList); + } catch (final Throwable e) { + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + ); + } + + private void returnBitmaps(final LongSparseArray thumbnailList) { + new MainThreadExecutor().runTask("", new Runnable() { + @Override + public void run() { + mBitmapList = thumbnailList; + invalidate(); + } + } + , 0L); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + + if (mBitmapList != null) { + canvas.save(); + int x = 0; + + for (int i = 0; i < mBitmapList.size(); i++) { + Bitmap bitmap = mBitmapList.get(i); + + if (bitmap != null) { + canvas.drawBitmap(bitmap, x, 0, null); + x = x + bitmap.getWidth(); + } + } + } + } + + public void setVideo(@NonNull Uri data) { + mVideoUri = data; + getBitmap(); + } + + public final class MainThreadExecutor { + + private final Handler HANDLER = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + Runnable callback = msg.getCallback(); + if (callback != null) { + callback.run(); + decrementToken((Token) msg.obj); + } else { + super.handleMessage(msg); + } + } + }; + + private final Map TOKENS = new HashMap<>(); + + private MainThreadExecutor() { + // should not be instantiated + } + + /** + * Store a new task in the map for providing cancellation. This method is + * used by AndroidAnnotations and not intended to be called by clients. + * + * @param id the identifier of the task + * @param task the task itself + * @param delay the delay or zero to run immediately + */ + public void runTask(String id, Runnable task, long delay) { + if ("".equals(id)) { + HANDLER.postDelayed(task, delay); + return; + } + long time = SystemClock.uptimeMillis() + delay; + HANDLER.postAtTime(task, nextToken(id), time); + } + + private Token nextToken(String id) { + synchronized (TOKENS) { + Token token = TOKENS.get(id); + if (token == null) { + token = new Token(id); + TOKENS.put(id, token); + } + token.runnablesCount++; + return token; + } + } + + private void decrementToken(Token token) { + synchronized (TOKENS) { + if (--token.runnablesCount == 0) { + String id = token.id; + Token old = TOKENS.remove(id); + if (old != token) { + // a runnable finished after cancelling, we just removed a + // wrong token, lets put it back + TOKENS.put(id, old); + } + } + } + } + + /** + * Cancel all tasks having the specified id. + * + * @param id the cancellation identifier + */ + public void cancelAll(String id) { + Token token; + synchronized (TOKENS) { + token = TOKENS.remove(id); + } + if (token == null) { + // nothing to cancel + return; + } + HANDLER.removeCallbacksAndMessages(token); + } + + private final class Token { + int runnablesCount = 0; + final String id; + + private Token(String id) { + this.id = id; + } + } + + } + +} diff --git a/android/src/main/res/drawable-mdpi/circle_thumb.xml b/android/src/main/res/drawable-mdpi/circle_thumb.xml new file mode 100644 index 0000000..39d0f8e --- /dev/null +++ b/android/src/main/res/drawable-mdpi/circle_thumb.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/android/src/main/res/drawable-mdpi/ic_white_pause.png b/android/src/main/res/drawable-mdpi/ic_white_pause.png new file mode 100644 index 0000000..15e08b5 Binary files /dev/null and b/android/src/main/res/drawable-mdpi/ic_white_pause.png differ diff --git a/android/src/main/res/drawable-mdpi/ic_white_play.png b/android/src/main/res/drawable-mdpi/ic_white_play.png new file mode 100644 index 0000000..33169d5 Binary files /dev/null and b/android/src/main/res/drawable-mdpi/ic_white_play.png differ diff --git a/android/src/main/res/drawable-mdpi/seekbar_drawable_video.xml b/android/src/main/res/drawable-mdpi/seekbar_drawable_video.xml new file mode 100644 index 0000000..46ca651 --- /dev/null +++ b/android/src/main/res/drawable-mdpi/seekbar_drawable_video.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/res/drawable-v24/time_line_a.png b/android/src/main/res/drawable-v24/time_line_a.png new file mode 100644 index 0000000..6050902 Binary files /dev/null and b/android/src/main/res/drawable-v24/time_line_a.png differ diff --git a/android/src/main/res/drawable/background_button.xml b/android/src/main/res/drawable/background_button.xml deleted file mode 100644 index 551b010..0000000 --- a/android/src/main/res/drawable/background_button.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/src/main/res/drawable/ic_baseline_clear_24.xml b/android/src/main/res/drawable/ic_baseline_clear_24.xml deleted file mode 100644 index af9bff5..0000000 --- a/android/src/main/res/drawable/ic_baseline_clear_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/src/main/res/drawable/ic_baseline_done_24.xml b/android/src/main/res/drawable/ic_baseline_done_24.xml deleted file mode 100644 index dc9b7e7..0000000 --- a/android/src/main/res/drawable/ic_baseline_done_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml b/android/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml deleted file mode 100644 index 9474032..0000000 --- a/android/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/src/main/res/drawable/ic_videocam_black_24dp.xml b/android/src/main/res/drawable/ic_videocam_black_24dp.xml deleted file mode 100644 index 923a07f..0000000 --- a/android/src/main/res/drawable/ic_videocam_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/src/main/res/drawable/icon_video_play.png b/android/src/main/res/drawable/icon_video_play.png deleted file mode 100644 index 0498cad..0000000 Binary files a/android/src/main/res/drawable/icon_video_play.png and /dev/null differ diff --git a/android/src/main/res/drawable/launch_background.xml b/android/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/android/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/src/main/res/layout/activity_main.xml b/android/src/main/res/layout/activity_main.xml deleted file mode 100644 index 4f22107..0000000 --- a/android/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - diff --git a/android/src/main/res/layout/activity_trimmer.xml b/android/src/main/res/layout/activity_trimmer.xml index 020816a..ae2f109 100644 --- a/android/src/main/res/layout/activity_trimmer.xml +++ b/android/src/main/res/layout/activity_trimmer.xml @@ -2,10 +2,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/res/layout/video_trimmer.xml b/android/src/main/res/layout/video_trimmer.xml deleted file mode 100644 index 7e61ae1..0000000 --- a/android/src/main/res/layout/video_trimmer.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/src/main/res/values/colors.xml b/android/src/main/res/values/colors.xml index 3ab3e9c..e6431ba 100644 --- a/android/src/main/res/values/colors.xml +++ b/android/src/main/res/values/colors.xml @@ -3,4 +3,12 @@ #3F51B5 #303F9F #FF4081 + + #65b7b7b7 + #0e1f2f + #000000 + #3be3e3 + #FF15FF00 + #00000000 + #ffffff diff --git a/android/src/main/res/values/dimen.xml b/android/src/main/res/values/dimen.xml new file mode 100644 index 0000000..a6dfba3 --- /dev/null +++ b/android/src/main/res/values/dimen.xml @@ -0,0 +1,14 @@ + + + + @dimen/_8sdp + @dimen/_5sdp + @dimen/_165sdp + @dimen/_28sdp + + @dimen/_19ssp + @dimen/_12sdp + + 62dp + 150dp + diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 6dc2c0e..7a501ee 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -6,5 +6,10 @@ Video saves at : $%1s Select or record a a video below to try it out: sec - + Save + Cancel + Done + Select 1 min video + Video should be of minimum 3 seconds + Should allow permission to work app correct diff --git a/android/src/main/res/values/styles.xml b/android/src/main/res/values/styles.xml index e917519..949fbe6 100644 --- a/android/src/main/res/values/styles.xml +++ b/android/src/main/res/values/styles.xml @@ -1,12 +1,5 @@ - - - + + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index bc73f07..9d54427 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -40,8 +40,10 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.steelkiwi.videotrimming_example" - minSdkVersion 21 - targetSdkVersion 29 + minSdkVersion 26 + targetSdkVersion 30 + multiDexEnabled true + versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -61,4 +63,6 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:multidex:1.0.3' + } diff --git a/example/android/build.gradle b/example/android/build.gradle index 82cbb58..a6b142b 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146..59dd2e1 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 23 08:50:38 CEST 2017 +#Fri Mar 12 19:02:36 EET 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/example/lib/main.dart b/example/lib/main.dart index fd693f4..f257260 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -63,10 +63,12 @@ class _MyHomePageState extends State { } _pickVideo() async { if (await Permission.storage.request().isGranted) { - File selectedFile = await FilePicker.getFile(type: FileType.video); - selectedPath = selectedFile.path; + + FilePickerResult selectedFile = await FilePicker.platform.pickFiles(type: FileType.video); + + selectedPath = selectedFile.files.single.path; var trimmedFile = - await VideoTrimming.trimVideo(sourcePath: selectedFile.path); + await VideoTrimming.trimVideo(sourcePath: selectedPath); trimmedPath = trimmedFile.path; _controller = VideoPlayerController.file(trimmedFile); @@ -80,5 +82,5 @@ class _MyHomePageState extends State { ].request(); print(statuses[Permission.location]); - } + } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 559321a..bf66916 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -63,14 +63,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "1.11.0+3" - file_picker_platform_interface: - dependency: transitive - description: - name: file_picker_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" + version: "2.1.7" flutter: dependency: "direct main" description: flutter diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 134bb10..816bf08 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.3 - file_picker: ^1.10.0 + file_picker: ^2.1.4 permission_handler: ^5.0.1 video_player: ^0.10.11+1 dev_dependencies: