├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lb │ │ └── video_trimmer_sample │ │ ├── MainActivity.kt │ │ ├── ThirdPartyIntentsUtil.kt │ │ ├── TrimmerActivity.kt │ │ └── VideoTrimmerView.kt │ └── res │ ├── drawable-hdpi │ └── icon_video_play.png │ ├── drawable-v21 │ └── background_button.xml │ ├── drawable │ ├── background_button.xml │ ├── ic_photo_size_select_actual_black_24dp.xml │ └── ic_videocam_black_24dp.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_trimmer.xml │ └── video_trimmer.xml │ ├── menu │ └── main.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 │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot.png ├── settings.gradle └── video_trimmer_library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java └── com └── lb └── video_trimmer_library ├── BaseVideoTrimmerView.kt ├── interfaces ├── OnProgressVideoListener.kt ├── OnRangeSeekBarListener.kt └── VideoTrimmingListener.kt ├── utils ├── BackgroundExecutor.kt ├── FileUtils.kt ├── TrimVideoUtils.kt └── UiThreadExecutor.kt └── view ├── RangeSeekBarView.kt └── TimeLineView.kt /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | 45 | # Keystore files 46 | # Uncomment the following line if you do not want to check your keystore files in. 47 | #*.jks 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | 52 | # Google Services (e.g. APIs or Firebase) 53 | google-services.json 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md 66 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 18 | 19 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 AndroidDeveloperLB 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Warning: this is not maintained anymore. 2 | It's a fork of another library that I've found: 3 | https://github.com/titansgroup/k4l-video-trimmer 4 | 5 | You are free to send pull requests if you think they are good, though. 6 | 7 | ---- 8 | 9 | [![Release](https://img.shields.io/github/release/AndroidDeveloperLB/VideoTrimmer.svg?style=flat)](https://jitpack.io/#AndroidDeveloperLB/VideoTrimmer) 10 | 11 | # VideoTrimmer 12 | 13 | Allows to trim videos on Android, including UI. 14 | 15 | import using [Jitpack](https://jitpack.io/#AndroidDeveloperLB/VideoTrimmer) : 16 | 17 | allprojects { 18 | repositories { 19 | ... 20 | maven { url 'https://jitpack.io' } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation 'com.github.AndroidDeveloperLB:VideoTrimmer:#' 26 | } 27 | 28 | ![screenshot](https://github.com/AndroidDeveloperLB/VideoTrimmer/blob/master/screenshot.png?raw=true) 29 | 30 | - Code based on "[k4l-video-trimmer](https://github.com/titansgroup/k4l-video-trimmer)" library, to handle various issues on it, that I've asked about [here](https://stackoverflow.com/q/54503331/878126) and [here] . 31 | - Trimming is done by using "[mp4parser](https://github.com/sannies/mp4parser)" library (that was used on the original library) and on [this answer](https://stackoverflow.com/a/44653626/878126), which is based on the Gallery app of Android. 32 | - This library handled various issues that the original had, while also having 100% code in Kotlin. 33 | - At first it was a fork, but as it became very different in code, and because the original one isn't maintained anymore, I decided to create this one as a new repository. 34 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 28 7 | defaultConfig { 8 | applicationId "com.lb.video_trimmer_library" 9 | minSdkVersion 18 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 30 | implementation 'androidx.appcompat:appcompat:1.1.0-alpha02' 31 | implementation 'androidx.core:core-ktx:1.1.0-alpha04' 32 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 33 | implementation project(path: ':video_trimmer_library') 34 | implementation 'com.google.android.material:material:1.1.0-alpha03' 35 | } 36 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/lb/video_trimmer_sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lb.video_trimmer_sample 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.app.Activity 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.net.Uri 9 | import android.os.Bundle 10 | import android.provider.MediaStore 11 | import android.view.Menu 12 | import android.view.MenuItem 13 | import android.webkit.MimeTypeMap 14 | import android.widget.Toast 15 | import androidx.appcompat.app.AlertDialog 16 | import androidx.appcompat.app.AppCompatActivity 17 | import androidx.core.app.ActivityCompat 18 | import kotlinx.android.synthetic.main.activity_main.* 19 | import java.util.* 20 | 21 | 22 | class MainActivity : AppCompatActivity() { 23 | companion object { 24 | private const val REQUEST_VIDEO_TRIMMER = 1 25 | private const val REQUEST_STORAGE_READ_ACCESS_PERMISSION = 2 26 | internal const val EXTRA_INPUT_URI = "EXTRA_INPUT_URI" 27 | private val allowedVideoFileExtensions = arrayOf("mkv", "mp4", "3gp", "mov", "mts") 28 | private val videosMimeTypes = ArrayList(allowedVideoFileExtensions.size) 29 | } 30 | 31 | init { 32 | val mimeTypeMap = MimeTypeMap.getSingleton() 33 | for (fileExtension in allowedVideoFileExtensions) { 34 | val mimeTypeFromExtension = mimeTypeMap.getMimeTypeFromExtension(fileExtension) 35 | if (mimeTypeFromExtension != null) 36 | videosMimeTypes.add(mimeTypeFromExtension) 37 | } 38 | } 39 | 40 | @SuppressLint("StaticFieldLeak") 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | setContentView(R.layout.activity_main) 44 | galleryButton.setOnClickListener { pickFromGallery() } 45 | cameraButton.setOnClickListener { openVideoCapture() } 46 | } 47 | 48 | private fun openVideoCapture() { 49 | val videoCapture = Intent(MediaStore.ACTION_VIDEO_CAPTURE) 50 | startActivityForResult(videoCapture, REQUEST_VIDEO_TRIMMER) 51 | } 52 | 53 | private fun pickFromGallery() { 54 | if (ActivityCompat.checkSelfPermission( 55 | this, 56 | Manifest.permission.READ_EXTERNAL_STORAGE 57 | ) != PackageManager.PERMISSION_GRANTED 58 | ) { 59 | requestPermission( 60 | Manifest.permission.READ_EXTERNAL_STORAGE, 61 | getString(R.string.permission_read_storage_rationale), 62 | REQUEST_STORAGE_READ_ACCESS_PERMISSION 63 | ) 64 | } else { 65 | var intentForChoosingVideos = ThirdPartyIntentsUtil.getPickFileChooserIntent( 66 | this, 67 | null, 68 | false, 69 | true, 70 | "video/*", 71 | videosMimeTypes.toTypedArray(), 72 | null 73 | ) 74 | if (intentForChoosingVideos == null) 75 | intentForChoosingVideos = 76 | ThirdPartyIntentsUtil.getPickFileIntent(this, "video/*,", videosMimeTypes.toTypedArray()) 77 | if (intentForChoosingVideos != null) 78 | startActivityForResult(intentForChoosingVideos, REQUEST_VIDEO_TRIMMER) 79 | } 80 | } 81 | 82 | public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 83 | if (resultCode == Activity.RESULT_OK) { 84 | if (requestCode == REQUEST_VIDEO_TRIMMER) { 85 | val uri = data!!.data 86 | if (uri != null && checkIfUriCanBeUsedForVideo(uri)) { 87 | startTrimActivity(uri) 88 | } else { 89 | Toast.makeText(this@MainActivity, R.string.toast_cannot_retrieve_selected_video, Toast.LENGTH_SHORT) 90 | .show() 91 | } 92 | } 93 | } 94 | } 95 | 96 | private fun checkIfUriCanBeUsedForVideo(uri: Uri): Boolean { 97 | val mimeType = ThirdPartyIntentsUtil.getMimeType(this, uri) 98 | val identifiedAsVideo = mimeType != null && videosMimeTypes.contains(mimeType) 99 | if (!identifiedAsVideo) 100 | return false 101 | try { 102 | //check that it can be opened and trimmed using our technique 103 | val fileDescriptor = contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor 104 | val inputStream = (if (fileDescriptor == null) null else contentResolver.openInputStream(uri)) 105 | ?: return false 106 | inputStream.close() 107 | return true 108 | } catch (e: Exception) { 109 | return false 110 | } 111 | } 112 | 113 | private fun startTrimActivity(uri: Uri) { 114 | val intent = Intent(this, TrimmerActivity::class.java) 115 | intent.putExtra(EXTRA_INPUT_URI, uri) 116 | startActivity(intent) 117 | } 118 | 119 | /** 120 | * Requests given permission. 121 | * If the permission has been denied previously, a Dialog will prompt the user to grant the 122 | * permission, otherwise it is requested directly. 123 | */ 124 | private fun requestPermission(permission: String, rationale: String, requestCode: Int) { 125 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { 126 | val builder = AlertDialog.Builder(this) 127 | builder.setTitle(getString(R.string.permission_title_rationale)) 128 | builder.setMessage(rationale) 129 | builder.setPositiveButton(android.R.string.ok) { _, _ -> 130 | ActivityCompat.requestPermissions( 131 | this@MainActivity, 132 | arrayOf(permission), 133 | requestCode 134 | ) 135 | } 136 | builder.setNegativeButton(android.R.string.cancel, null) 137 | builder.show() 138 | } else { 139 | ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode) 140 | } 141 | } 142 | 143 | /** 144 | * Callback received when a permissions request has been completed. 145 | */ 146 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 147 | when (requestCode) { 148 | REQUEST_STORAGE_READ_ACCESS_PERMISSION -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 149 | pickFromGallery() 150 | } 151 | else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) 152 | } 153 | } 154 | 155 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 156 | menuInflater.inflate(R.menu.main, menu) 157 | return true 158 | } 159 | 160 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 161 | var url: String? = null 162 | when (item.itemId) { 163 | R.id.menuItem_all_my_apps -> url = "https://play.google.com/store/apps/developer?id=AndroidDeveloperLB" 164 | R.id.menuItem_all_my_repositories -> url = "https://github.com/AndroidDeveloperLB" 165 | R.id.menuItem_current_repository_website -> url = "https://github.com/AndroidDeveloperLB/VideoTrimmer" 166 | R.id.menuItem_show_recyclerViewSample -> { 167 | startActivity(Intent(this, MainActivity::class.java)) 168 | return true 169 | } 170 | } 171 | if (url == null) 172 | return true 173 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 174 | intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) 175 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 176 | startActivity(intent) 177 | return true 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /app/src/main/java/com/lb/video_trimmer_sample/ThirdPartyIntentsUtil.kt: -------------------------------------------------------------------------------- 1 | package com.lb.video_trimmer_sample 2 | 3 | import android.annotation.TargetApi 4 | import android.content.ComponentName 5 | import android.content.ContentResolver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.pm.PackageManager 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.os.Parcelable 12 | import android.provider.MediaStore 13 | import android.webkit.MimeTypeMap 14 | import java.util.* 15 | 16 | object ThirdPartyIntentsUtil { 17 | // https://medium.com/@louis993546/how-to-ask-system-to-open-intent-to-select-jpg-and-png-only-on-android-i-e-no-gif-e0491af240bf 18 | //example usage: mainType= "*/*" extraMimeTypes= arrayOf("image/*", "video/*") - choose all images and videos 19 | //example usage: mainType= "image/*" extraMimeTypes= arrayOf("image/jpeg", "image/png") - choose all images of png and jpeg types 20 | /**note that this only requests to choose the files, but it's not guaranteed that this is what you will get*/ 21 | @JvmStatic 22 | fun getPickFileIntent(context: Context, mainType: String = "*/*", extraMimeTypes: Array? = null): Intent? { 23 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) 24 | return null 25 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) 26 | intent.addCategory(Intent.CATEGORY_OPENABLE) 27 | intent.type = mainType 28 | if (!extraMimeTypes.isNullOrEmpty()) 29 | intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes) 30 | if (context.packageManager.queryIntentActivities(intent, 0).isNullOrEmpty()) 31 | return null 32 | return intent 33 | } 34 | 35 | // https://github.com/linchaolong/ImagePicker/blob/master/library/src/main/java/com/linchaolong/android/imagepicker/cropper/CropImage.java 36 | @JvmStatic 37 | fun getPickFileChooserIntent( 38 | context: Context, title: CharSequence?, preferDocuments: Boolean = true, includeCameraIntents: Boolean, mainType: String 39 | , extraMimeTypes: Array? = null, extraIntents: ArrayList? = null 40 | ): Intent? { 41 | val packageManager = context.packageManager 42 | var allIntents = 43 | getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT, mainType, extraMimeTypes) 44 | if (allIntents.isEmpty()) { 45 | // if no intents found for get-content try pick intent action (Huawei P9). 46 | allIntents = 47 | getGalleryIntents(packageManager, Intent.ACTION_PICK, mainType, extraMimeTypes) 48 | } 49 | if (includeCameraIntents) { 50 | val cameraIntents = getCameraIntents(packageManager) 51 | allIntents.addAll(0, cameraIntents) 52 | } 53 | // Log.d("AppLog", "got ${allIntents.size} intents") 54 | if (allIntents.isEmpty()) 55 | return null 56 | if (preferDocuments) 57 | for (intent in allIntents) 58 | if (intent.component!!.packageName == "com.android.documentsui") 59 | return intent 60 | if (allIntents.size == 1) 61 | return allIntents[0] 62 | var target: Intent? = null 63 | for ((index, intent) in allIntents.withIndex()) { 64 | if (intent.component!!.packageName == "com.android.documentsui") { 65 | target = intent 66 | allIntents.removeAt(index) 67 | break 68 | } 69 | } 70 | if (target == null) 71 | target = allIntents[allIntents.size - 1] 72 | allIntents.removeAt(allIntents.size - 1) 73 | // Create a chooser from the main intent 74 | val chooserIntent = Intent.createChooser(target, title) 75 | if (extraIntents != null && extraIntents.isNotEmpty()) 76 | allIntents.addAll(extraIntents) 77 | // Add all other intents 78 | chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toTypedArray()) 79 | return chooserIntent 80 | } 81 | 82 | private fun getCameraIntents(packageManager: PackageManager): ArrayList { 83 | val cameraIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) 84 | val listCamera = packageManager.queryIntentActivities(cameraIntent, 0) 85 | val intents = ArrayList() 86 | for (res in listCamera) { 87 | val intent = Intent(cameraIntent) 88 | intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name) 89 | intent.`package` = res.activityInfo.packageName 90 | intents.add(intent) 91 | } 92 | return intents 93 | } 94 | 95 | /** 96 | * Get all Gallery intents for getting image from one of the apps of the device that handle 97 | * images. Intent.ACTION_GET_CONTENT and then Intent.ACTION_PICK 98 | */ 99 | @TargetApi(Build.VERSION_CODES.KITKAT) 100 | private fun getGalleryIntents( 101 | packageManager: PackageManager, action: String, 102 | mainType: String, extraMimeTypes: Array? = null 103 | ): ArrayList { 104 | val galleryIntent = if (action == Intent.ACTION_GET_CONTENT) 105 | Intent(action) 106 | else 107 | Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) 108 | galleryIntent.type = mainType 109 | if (!extraMimeTypes.isNullOrEmpty()) { 110 | galleryIntent.addCategory(Intent.CATEGORY_OPENABLE) 111 | galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes) 112 | } 113 | val listGallery = packageManager.queryIntentActivities(galleryIntent, 0) 114 | val intents = ArrayList() 115 | for (res in listGallery) { 116 | val intent = Intent(galleryIntent) 117 | intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name) 118 | intent.`package` = res.activityInfo.packageName 119 | intents.add(intent) 120 | } 121 | return intents 122 | } 123 | 124 | @JvmStatic 125 | fun getMimeType(context: Context, uri: Uri): String? { 126 | return if (ContentResolver.SCHEME_CONTENT == uri.scheme) { 127 | val cr = context.contentResolver 128 | cr.getType(uri) 129 | } else { 130 | val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()) 131 | MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()) 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/lb/video_trimmer_sample/TrimmerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lb.video_trimmer_sample 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.lb.video_trimmer_library.interfaces.VideoTrimmingListener 10 | import kotlinx.android.synthetic.main.activity_trimmer.* 11 | import java.io.File 12 | 13 | class TrimmerActivity : AppCompatActivity(), VideoTrimmingListener { 14 | // private var progressDialog: ProgressDialog? = null 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_trimmer) 19 | val inputVideoUri: Uri? = intent?.getParcelableExtra(MainActivity.EXTRA_INPUT_URI) 20 | if (inputVideoUri == null) { 21 | finish() 22 | return 23 | } 24 | //setting progressbar 25 | // progressDialog = ProgressDialog(this) 26 | // progressDialog!!.setCancelable(false) 27 | // progressDialog!!.setMessage(getString(R.string.trimming_progress)) 28 | videoTrimmerView.setMaxDurationInMs(10 * 1000) 29 | videoTrimmerView.setOnK4LVideoListener(this) 30 | val parentFolder = getExternalFilesDir(null)!! 31 | parentFolder.mkdirs() 32 | val fileName = "trimmedVideo_${System.currentTimeMillis()}.mp4" 33 | val trimmedVideoFile = File(parentFolder, fileName) 34 | videoTrimmerView.setDestinationFile(trimmedVideoFile) 35 | videoTrimmerView.setVideoURI(inputVideoUri) 36 | videoTrimmerView.setVideoInformationVisibility(true) 37 | } 38 | 39 | override fun onTrimStarted() { 40 | trimmingProgressView.visibility = View.VISIBLE 41 | } 42 | 43 | override fun onFinishedTrimming(uri: Uri?) { 44 | trimmingProgressView.visibility = View.GONE 45 | if (uri == null) { 46 | Toast.makeText(this@TrimmerActivity, "failed trimming", Toast.LENGTH_SHORT).show() 47 | } else { 48 | val msg = getString(R.string.video_saved_at, uri.path) 49 | Toast.makeText(this@TrimmerActivity, msg, Toast.LENGTH_SHORT).show() 50 | val intent = Intent(Intent.ACTION_VIEW, uri) 51 | intent.setDataAndType(uri, "video/mp4") 52 | startActivity(intent) 53 | } 54 | finish() 55 | } 56 | 57 | override fun onErrorWhileViewingVideo(what: Int, extra: Int) { 58 | trimmingProgressView.visibility = View.GONE 59 | Toast.makeText(this@TrimmerActivity, "error while previewing video", Toast.LENGTH_SHORT).show() 60 | } 61 | 62 | override fun onVideoPrepared() { 63 | // Toast.makeText(TrimmerActivity.this, "onVideoPrepared", Toast.LENGTH_SHORT).show(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/lb/video_trimmer_sample/VideoTrimmerView.kt: -------------------------------------------------------------------------------- 1 | package com.lb.video_trimmer_sample 2 | 3 | import android.content.Context 4 | import android.text.format.Formatter 5 | import android.util.AttributeSet 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.widget.VideoView 9 | import com.lb.video_trimmer_library.BaseVideoTrimmerView 10 | import com.lb.video_trimmer_library.view.RangeSeekBarView 11 | import com.lb.video_trimmer_library.view.TimeLineView 12 | import kotlinx.android.synthetic.main.video_trimmer.view.* 13 | 14 | class VideoTrimmerView @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : BaseVideoTrimmerView(context, attrs, defStyleAttr) { 15 | private fun stringForTime(timeMs: Int): String { 16 | val totalSeconds = timeMs / 1000 17 | val seconds = totalSeconds % 60 18 | val minutes = totalSeconds / 60 % 60 19 | val hours = totalSeconds / 3600 20 | val timeFormatter = java.util.Formatter() 21 | return if (hours > 0) 22 | timeFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() 23 | else 24 | timeFormatter.format("%02d:%02d", minutes, seconds).toString() 25 | } 26 | 27 | override fun initRootView() { 28 | LayoutInflater.from(context).inflate(R.layout.video_trimmer, this, true) 29 | fab.setOnClickListener { initiateTrimming() } 30 | } 31 | 32 | override fun getTimeLineView(): TimeLineView = timeLineView 33 | 34 | override fun getTimeInfoContainer(): View = timeTextContainer 35 | 36 | override fun getPlayView(): View = playIndicatorView 37 | 38 | override fun getVideoView(): VideoView = videoView 39 | 40 | override fun getVideoViewContainer(): View = videoViewContainer 41 | 42 | override fun getRangeSeekBarView(): RangeSeekBarView = rangeSeekBarView 43 | 44 | override fun onRangeUpdated(startTimeInMs: Int, endTimeInMs: Int) { 45 | val seconds = context.getString(R.string.short_seconds) 46 | trimTimeRangeTextView.text = "${stringForTime(startTimeInMs)} $seconds - ${stringForTime(endTimeInMs)} $seconds" 47 | } 48 | 49 | override fun onVideoPlaybackReachingTime(timeInMs: Int) { 50 | val seconds = context.getString(R.string.short_seconds) 51 | playbackTimeTextView.text = "${stringForTime(timeInMs)} $seconds" 52 | } 53 | 54 | override fun onGotVideoFileSize(videoFileSize: Long) { 55 | videoFileSizeTextView.text = Formatter.formatShortFileSize(context, videoFileSize) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/icon_video_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/app/src/main/res/drawable-hdpi/icon_video_play.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_photo_size_select_actual_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_videocam_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 20 | 21 | 23 | 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_trimmer.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 13 | 14 | 16 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/video_trimmer.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 20 | 21 | 22 | 23 | 28 | 29 | 32 | 33 | 34 | 36 | 39 | 40 | 44 | 45 | 46 | 50 | 51 | 55 | 56 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VideoTrimmerSample 3 | Permission needed 4 | Storage read permission is needed to pick files 5 | Cannot retrieve selected video 6 | Video saves at : $%1s 7 | Select or record a a video below to try it out: 8 | sec 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.21' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.3.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | task clean(type: Delete) { 25 | delete rootProject.buildDir 26 | } 27 | -------------------------------------------------------------------------------- /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=-Xmx1536m 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 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Feb 11 11:02:23 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-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndroidDeveloperLB/VideoTrimmer/c15f1be0a1f9c7692318a1e78ce7680e2ae3bb65/screenshot.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':video_trimmer_library' 2 | -------------------------------------------------------------------------------- /video_trimmer_library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /video_trimmer_library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | android { 5 | compileSdkVersion 28 6 | 7 | defaultConfig { 8 | minSdkVersion 18 9 | targetSdkVersion 28 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 | compileOptions { 21 | sourceCompatibility JavaVersion.VERSION_1_8 22 | targetCompatibility JavaVersion.VERSION_1_8 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(include: ['*.jar'], dir: 'libs') 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 29 | implementation 'androidx.appcompat:appcompat:1.0.2' 30 | implementation 'com.googlecode.mp4parser:isoparser:1.1.22' 31 | 32 | } 33 | -------------------------------------------------------------------------------- /video_trimmer_library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/BaseVideoTrimmerView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | @file:Suppress("LeakingThis") 25 | 26 | package com.lb.video_trimmer_library 27 | 28 | import android.annotation.SuppressLint 29 | import android.content.Context 30 | import android.media.MediaMetadataRetriever 31 | import android.media.MediaPlayer 32 | import android.net.Uri 33 | import android.os.Handler 34 | import android.os.Message 35 | import android.provider.OpenableColumns 36 | import android.util.AttributeSet 37 | import android.view.GestureDetector 38 | import android.view.MotionEvent 39 | import android.view.View 40 | import android.widget.FrameLayout 41 | import android.widget.VideoView 42 | import androidx.annotation.UiThread 43 | import com.lb.video_trimmer_library.interfaces.OnProgressVideoListener 44 | import com.lb.video_trimmer_library.interfaces.OnRangeSeekBarListener 45 | import com.lb.video_trimmer_library.interfaces.VideoTrimmingListener 46 | import com.lb.video_trimmer_library.utils.BackgroundExecutor 47 | import com.lb.video_trimmer_library.utils.TrimVideoUtils 48 | import com.lb.video_trimmer_library.utils.UiThreadExecutor 49 | import com.lb.video_trimmer_library.view.RangeSeekBarView 50 | import com.lb.video_trimmer_library.view.TimeLineView 51 | import java.io.File 52 | import java.lang.ref.WeakReference 53 | 54 | abstract class BaseVideoTrimmerView @JvmOverloads constructor( 55 | context: Context, 56 | attrs: AttributeSet, 57 | defStyleAttr: Int = 0 58 | ) : FrameLayout(context, attrs, defStyleAttr) { 59 | private val rangeSeekBarView: RangeSeekBarView 60 | private val videoViewContainer: View 61 | private val timeInfoContainer: View 62 | private val videoView: VideoView 63 | private val playView: View 64 | private val timeLineView: TimeLineView 65 | private var src: Uri? = null 66 | private var dstFile: File? = null 67 | private var maxDurationInMs: Int = 0 68 | private var listeners = ArrayList() 69 | private var videoTrimmingListener: VideoTrimmingListener? = null 70 | private var duration = 0 71 | private var timeVideo = 0 72 | private var startPosition = 0 73 | private var endPosition = 0 74 | private var originSizeFile: Long = 0 75 | private var resetSeekBar = true 76 | private val messageHandler = MessageHandler(this) 77 | 78 | init { 79 | initRootView() 80 | rangeSeekBarView = getRangeSeekBarView() 81 | videoViewContainer = getVideoViewContainer() 82 | videoView = getVideoView() 83 | playView = getPlayView() 84 | timeInfoContainer = getTimeInfoContainer() 85 | timeLineView = getTimeLineView() 86 | setUpListeners() 87 | setUpMargins() 88 | } 89 | 90 | abstract fun initRootView() 91 | 92 | abstract fun getTimeLineView(): TimeLineView 93 | 94 | abstract fun getTimeInfoContainer(): View 95 | 96 | abstract fun getPlayView(): View 97 | 98 | abstract fun getVideoView(): VideoView 99 | 100 | abstract fun getVideoViewContainer(): View 101 | 102 | abstract fun getRangeSeekBarView(): RangeSeekBarView 103 | 104 | abstract fun onRangeUpdated(startTimeInMs: Int, endTimeInMs: Int) 105 | 106 | /**occurs during playback, to tell that you've reached a specific time in the video*/ 107 | abstract fun onVideoPlaybackReachingTime(timeInMs: Int) 108 | 109 | abstract fun onGotVideoFileSize(videoFileSize: Long) 110 | 111 | @SuppressLint("ClickableViewAccessibility") 112 | private fun setUpListeners() { 113 | listeners.add(object : OnProgressVideoListener { 114 | override fun updateProgress(time: Int, max: Int, scale: Float) { 115 | this@BaseVideoTrimmerView.updateVideoProgress(time) 116 | } 117 | }) 118 | val gestureDetector = GestureDetector(context, 119 | object : GestureDetector.SimpleOnGestureListener() { 120 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean { 121 | onClickVideoPlayPause() 122 | return true 123 | } 124 | } 125 | ) 126 | videoView.setOnErrorListener { _, what, extra -> 127 | if (videoTrimmingListener != null) 128 | videoTrimmingListener!!.onErrorWhileViewingVideo(what, extra) 129 | false 130 | } 131 | 132 | videoView.setOnTouchListener { v, event -> 133 | gestureDetector.onTouchEvent(event) 134 | true 135 | } 136 | 137 | rangeSeekBarView.addOnRangeSeekBarListener(object : OnRangeSeekBarListener { 138 | override fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 139 | // Do nothing 140 | } 141 | 142 | override fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 143 | onSeekThumbs(index, value) 144 | } 145 | 146 | override fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 147 | // Do nothing 148 | } 149 | 150 | override fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 151 | onStopSeekThumbs() 152 | } 153 | }) 154 | videoView.setOnPreparedListener { this.onVideoPrepared(it) } 155 | videoView.setOnCompletionListener { onVideoCompleted() } 156 | } 157 | 158 | private fun setUpMargins() { 159 | val marge = rangeSeekBarView.thumbWidth 160 | val lp: MarginLayoutParams = timeLineView.layoutParams as MarginLayoutParams 161 | lp.setMargins(marge, lp.topMargin, marge, lp.bottomMargin) 162 | timeLineView.layoutParams = lp 163 | } 164 | 165 | @Suppress("unused") 166 | @UiThread 167 | fun initiateTrimming() { 168 | pauseVideo() 169 | val mediaMetadataRetriever = MediaMetadataRetriever() 170 | mediaMetadataRetriever.setDataSource(context, src) 171 | val metadataKeyDuration = 172 | java.lang.Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) 173 | if (timeVideo < MIN_TIME_FRAME) { 174 | if (metadataKeyDuration - endPosition > MIN_TIME_FRAME - timeVideo) { 175 | endPosition += MIN_TIME_FRAME - timeVideo 176 | } else if (startPosition > MIN_TIME_FRAME - timeVideo) { 177 | startPosition -= MIN_TIME_FRAME - timeVideo 178 | } 179 | } 180 | //notify that video trimming started 181 | if (videoTrimmingListener != null) 182 | videoTrimmingListener!!.onTrimStarted() 183 | BackgroundExecutor.execute( 184 | object : BackgroundExecutor.Task(null, 0L, null) { 185 | override fun execute() { 186 | try { 187 | TrimVideoUtils.startTrim( 188 | context, 189 | src!!, 190 | dstFile!!, 191 | startPosition.toLong(), 192 | endPosition.toLong(), 193 | duration.toLong(), 194 | videoTrimmingListener!! 195 | ) 196 | } catch (e: Throwable) { 197 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e) 198 | } 199 | } 200 | } 201 | ) 202 | } 203 | 204 | private fun onClickVideoPlayPause() { 205 | if (videoView.isPlaying) { 206 | messageHandler.removeMessages(SHOW_PROGRESS) 207 | pauseVideo() 208 | } else { 209 | playView.visibility = View.GONE 210 | if (resetSeekBar) { 211 | resetSeekBar = false 212 | videoView.seekTo(startPosition) 213 | } 214 | messageHandler.sendEmptyMessage(SHOW_PROGRESS) 215 | videoView.start() 216 | } 217 | } 218 | 219 | @UiThread 220 | private fun onVideoPrepared(mp: MediaPlayer) { 221 | // Adjust the size of the video 222 | // so it fits on the screen 223 | val videoWidth = mp.videoWidth 224 | val videoHeight = mp.videoHeight 225 | val videoProportion = videoWidth.toFloat() / videoHeight.toFloat() 226 | val screenWidth = videoViewContainer.width 227 | val screenHeight = videoViewContainer.height 228 | val screenProportion = screenWidth.toFloat() / screenHeight.toFloat() 229 | val lp = videoView.layoutParams 230 | if (videoProportion > screenProportion) { 231 | lp.width = screenWidth 232 | lp.height = (screenWidth.toFloat() / videoProportion).toInt() 233 | } else { 234 | lp.width = (videoProportion * screenHeight.toFloat()).toInt() 235 | lp.height = screenHeight 236 | } 237 | videoView.layoutParams = lp 238 | playView.visibility = View.VISIBLE 239 | duration = videoView.duration 240 | setSeekBarPosition() 241 | onRangeUpdated(startPosition, endPosition) 242 | onVideoPlaybackReachingTime(0) 243 | if (videoTrimmingListener != null) 244 | videoTrimmingListener!!.onVideoPrepared() 245 | } 246 | 247 | private fun setSeekBarPosition() { 248 | if (duration >= maxDurationInMs) { 249 | startPosition = duration / 2 - maxDurationInMs / 2 250 | endPosition = duration / 2 + maxDurationInMs / 2 251 | rangeSeekBarView.setThumbValue(0, startPosition * 100f / duration) 252 | rangeSeekBarView.setThumbValue(1, endPosition * 100f / duration) 253 | } else { 254 | startPosition = 0 255 | endPosition = duration 256 | } 257 | setProgressBarPosition(startPosition) 258 | videoView.seekTo(startPosition) 259 | timeVideo = duration 260 | rangeSeekBarView.initMaxWidth() 261 | } 262 | 263 | private fun onSeekThumbs(index: Int, value: Float) { 264 | when (index) { 265 | RangeSeekBarView.ThumbType.LEFT.index -> { 266 | startPosition = (duration * value / 100L).toInt() 267 | videoView.seekTo(startPosition) 268 | } 269 | RangeSeekBarView.ThumbType.RIGHT.index -> { 270 | endPosition = (duration * value / 100L).toInt() 271 | } 272 | } 273 | setProgressBarPosition(startPosition) 274 | 275 | onRangeUpdated(startPosition, endPosition) 276 | timeVideo = endPosition - startPosition 277 | } 278 | 279 | private fun onStopSeekThumbs() { 280 | messageHandler.removeMessages(SHOW_PROGRESS) 281 | pauseVideo() 282 | } 283 | 284 | private fun onVideoCompleted() { 285 | videoView.seekTo(startPosition) 286 | } 287 | 288 | private fun notifyProgressUpdate(all: Boolean) { 289 | if (duration == 0) return 290 | val position = videoView.currentPosition 291 | if (all) 292 | for (item in listeners) 293 | item.updateProgress(position, duration, position * 100f / duration) 294 | else 295 | listeners[1].updateProgress(position, duration, position * 100f / duration) 296 | } 297 | 298 | private fun updateVideoProgress(time: Int) { 299 | if (time >= endPosition) { 300 | messageHandler.removeMessages(SHOW_PROGRESS) 301 | pauseVideo() 302 | resetSeekBar = true 303 | return 304 | } 305 | setProgressBarPosition(time) 306 | onVideoPlaybackReachingTime(time) 307 | } 308 | 309 | @Suppress("MemberVisibilityCanBePrivate") 310 | fun pauseVideo() { 311 | videoView.pause() 312 | playView.visibility = View.VISIBLE 313 | } 314 | 315 | private fun setProgressBarPosition(position: Int) { 316 | if (duration > 0) { 317 | } 318 | } 319 | 320 | /** 321 | * Set video information visibility. 322 | * For now this is for debugging 323 | * 324 | * @param visible whether or not the videoInformation will be visible 325 | */ 326 | fun setVideoInformationVisibility(visible: Boolean) { 327 | timeInfoContainer.visibility = if (visible) View.VISIBLE else View.GONE 328 | } 329 | 330 | /** 331 | * Listener for some [VideoView] events 332 | * 333 | * @param onK4LVideoListener interface for events 334 | */ 335 | fun setOnK4LVideoListener(onK4LVideoListener: VideoTrimmingListener) { 336 | this.videoTrimmingListener = onK4LVideoListener 337 | } 338 | 339 | fun setDestinationFile(dst: File) { 340 | this.dstFile = dst 341 | } 342 | 343 | override fun onDetachedFromWindow() { 344 | super.onDetachedFromWindow() 345 | //Cancel all current operations 346 | BackgroundExecutor.cancelAll("", true) 347 | UiThreadExecutor.cancelAll("") 348 | } 349 | 350 | /** 351 | * Set the maximum duration of the trimmed video. 352 | * The trimmer interface wont allow the user to set duration longer than maxDuration 353 | */ 354 | fun setMaxDurationInMs(maxDurationInMs: Int) { 355 | this.maxDurationInMs = maxDurationInMs 356 | } 357 | 358 | /** 359 | * Sets the uri of the video to be trimmer 360 | * 361 | * @param videoURI Uri of the video 362 | */ 363 | fun setVideoURI(videoURI: Uri) { 364 | src = videoURI 365 | if (originSizeFile == 0L) { 366 | val cursor = context.contentResolver.query(videoURI, null, null, null, null) 367 | if (cursor != null) { 368 | val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) 369 | cursor.moveToFirst() 370 | originSizeFile = cursor.getLong(sizeIndex) 371 | cursor.close() 372 | onGotVideoFileSize(originSizeFile) 373 | } 374 | } 375 | videoView.setVideoURI(src) 376 | videoView.requestFocus() 377 | timeLineView.setVideo(src!!) 378 | } 379 | 380 | private class MessageHandler internal constructor(view: BaseVideoTrimmerView) : Handler() { 381 | private val mView: WeakReference = WeakReference(view) 382 | 383 | override fun handleMessage(msg: Message?) { 384 | val view = mView.get() 385 | if (view?.videoView == null) 386 | return 387 | view.notifyProgressUpdate(true) 388 | if (view.videoView.isPlaying) { 389 | sendEmptyMessageDelayed(0, 10) 390 | } 391 | } 392 | } 393 | 394 | companion object { 395 | private const val MIN_TIME_FRAME = 1000 396 | private const val SHOW_PROGRESS = 2 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/interfaces/OnProgressVideoListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.interfaces 25 | 26 | interface OnProgressVideoListener { 27 | fun updateProgress(time: Int, max: Int, scale: Float) 28 | } 29 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/interfaces/OnRangeSeekBarListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.interfaces 25 | 26 | import com.lb.video_trimmer_library.view.RangeSeekBarView 27 | 28 | interface OnRangeSeekBarListener { 29 | fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 30 | 31 | fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 32 | 33 | fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 34 | 35 | fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) 36 | } 37 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/interfaces/VideoTrimmingListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.interfaces 25 | 26 | import android.net.Uri 27 | import androidx.annotation.UiThread 28 | 29 | interface VideoTrimmingListener { 30 | @UiThread 31 | fun onVideoPrepared() 32 | 33 | @UiThread 34 | fun onTrimStarted() 35 | 36 | /** 37 | * @param uri the result, trimmed video, or null if failed 38 | */ 39 | @UiThread 40 | fun onFinishedTrimming(uri: Uri?) 41 | 42 | /** 43 | * check {[android.media.MediaPlayer.OnErrorListener]} 44 | */ 45 | @UiThread 46 | fun onErrorWhileViewingVideo(what: Int, extra: Int) 47 | } 48 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/utils/BackgroundExecutor.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2010-2016 eBusiness Information, Excilys Group 3 | * 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | * use this file except in compliance with the License. You may obtain a copy of 7 | * the License at 8 | * 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * 13 | * Unless required by applicable law or agreed To in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | * License for the specific language governing permissions and limitations under 17 | * the License. 18 | */ 19 | package com.lb.video_trimmer_library.utils 20 | 21 | import java.util.* 22 | import java.util.concurrent.* 23 | import java.util.concurrent.atomic.AtomicBoolean 24 | 25 | internal object BackgroundExecutor { 26 | private val DEFAULT_EXECUTOR: Executor = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors()) 27 | private val executor = DEFAULT_EXECUTOR 28 | private val TASKS = ArrayList() 29 | private val CURRENT_SERIAL = ThreadLocal() 30 | 31 | /** 32 | * Execute a runnable after the given delay. 33 | * 34 | * @param runnable the task to execute 35 | * @param delay the time from now to delay execution, in milliseconds 36 | * 37 | * 38 | * if `delay` is strictly positive and the current 39 | * executor does not support scheduling (if 40 | * Executor has been called with such an 41 | * executor) 42 | * @return Future associated to the running task 43 | * @throws IllegalArgumentException if the current executor set by Executor 44 | * does not support scheduling 45 | */ 46 | private fun directExecute(runnable: Runnable, delay: Long): Future<*>? { 47 | var future: Future<*>? = null 48 | if (delay > 0) { 49 | /* no serial, but a delay: schedule the task */ 50 | if (executor !is ScheduledExecutorService) { 51 | throw IllegalArgumentException("The executor set does not support scheduling") 52 | } 53 | future = executor.schedule(runnable, delay, TimeUnit.MILLISECONDS) 54 | } else { 55 | if (executor is ExecutorService) { 56 | future = executor.submit(runnable) 57 | } else { 58 | /* non-cancellable task */ 59 | executor.execute(runnable) 60 | } 61 | } 62 | return future 63 | } 64 | 65 | /** 66 | * Execute a task after (at least) its delay **and** after all 67 | * tasks added with the same non-null `serial` (if any) have 68 | * completed execution. 69 | * 70 | * @param task the task to execute 71 | * @throws IllegalArgumentException if `task.delay` is strictly positive and the 72 | * current executor does not support scheduling (if 73 | * Executor has been called with such an 74 | * executor) 75 | */ 76 | @Synchronized 77 | fun execute(task: Task) { 78 | var future: Future<*>? = null 79 | if (task.serial == null || !hasSerialRunning(task.serial)) { 80 | task.executionAsked = true 81 | future = directExecute(task, task.remainingDelay) 82 | } 83 | if ((task.id != null || task.serial != null) && !task.managed.get()) { 84 | /* keep task */ 85 | task.future = future 86 | TASKS.add(task) 87 | } 88 | } 89 | 90 | /** 91 | * Indicates whether a task with the specified `serial` has been 92 | * submitted to the executor. 93 | * 94 | * @param serial the serial queue 95 | * @return `true` if such a task has been submitted, 96 | * `false` otherwise 97 | */ 98 | private fun hasSerialRunning(serial: String): Boolean { 99 | for (task in TASKS) { 100 | if (task.executionAsked && serial == task.serial) { 101 | return true 102 | } 103 | } 104 | return false 105 | } 106 | 107 | /** 108 | * Retrieve and remove the first task having the specified 109 | * `serial` (if any). 110 | * 111 | * @param serial the serial queue 112 | * @return task if found, `null` otherwise 113 | */ 114 | private fun take(serial: String): Task? { 115 | val len = TASKS.size 116 | for (i in 0 until len) { 117 | if (serial == TASKS[i].serial) { 118 | return TASKS.removeAt(i) 119 | } 120 | } 121 | return null 122 | } 123 | 124 | /** 125 | * Cancel all tasks having the specified `id`. 126 | * 127 | * @param id the cancellation identifier 128 | * @param mayInterruptIfRunning `true` if the thread executing this task should be 129 | * interrupted; otherwise, in-progress tasks are allowed to 130 | * complete 131 | */ 132 | @Synchronized 133 | fun cancelAll(id: String, mayInterruptIfRunning: Boolean) { 134 | for (i in TASKS.indices.reversed()) { 135 | val task = TASKS[i] 136 | if (id == task.id) { 137 | if (task.future != null) { 138 | task.future!!.cancel(mayInterruptIfRunning) 139 | if (!task.managed.getAndSet(true)) { 140 | /* 141 | * the task has been submitted to the executor, but its 142 | * execution has not started yet, so that its run() 143 | * method will never call postExecute() 144 | */ 145 | task.postExecute() 146 | } 147 | } else if (task.executionAsked) { 148 | //Log.w(TAG, "A task with id " + task.id + " cannot be cancelled (the executor set does not support it)"); 149 | } else { 150 | /* this task has not been submitted to the executor */ 151 | TASKS.removeAt(i) 152 | } 153 | } 154 | } 155 | } 156 | 157 | abstract class Task(val id: String?, delay: Long, val serial: String?) : Runnable { 158 | // private var id: String? = null 159 | internal var remainingDelay: Long = 0 160 | private val targetTimeMillis: Long /* since epoch */ 161 | // private var serial: String? = null 162 | internal var executionAsked: Boolean = false 163 | internal var future: Future<*>? = null 164 | 165 | /* 166 | * A task can be cancelled after it has been submitted to the executor 167 | * but before its run() method is called. In that case, run() will never 168 | * be called, hence neither will postExecute(): the tasks with the same 169 | * serial identifier (if any) will never be submitted. 170 | * 171 | * Therefore, cancelAll() *must* call postExecute() if run() is not 172 | * started. 173 | * 174 | * This flag guarantees that either cancelAll() or run() manages this 175 | * task post execution, but not both. 176 | */ 177 | internal val managed = AtomicBoolean() 178 | 179 | init { 180 | // if ("" != id) { 181 | // this.id = id 182 | // } 183 | if (delay > 0) { 184 | remainingDelay = delay 185 | targetTimeMillis = System.currentTimeMillis() + delay 186 | } else targetTimeMillis = 0L 187 | // if ("" != serial) { 188 | // this.serial = serial 189 | // } 190 | } 191 | 192 | override fun run() { 193 | if (managed.getAndSet(true)) { 194 | /* cancelled and postExecute() already called */ 195 | return 196 | } 197 | 198 | try { 199 | CURRENT_SERIAL.set(serial) 200 | execute() 201 | } finally { 202 | /* handle next tasks */ 203 | postExecute() 204 | } 205 | } 206 | 207 | abstract fun execute() 208 | 209 | internal fun postExecute() { 210 | if (id == null && serial == null) { 211 | /* nothing to do */ 212 | return 213 | } 214 | CURRENT_SERIAL.set(null) 215 | synchronized(BackgroundExecutor::class.java) { 216 | /* execution complete */ 217 | TASKS.remove(this) 218 | if (serial != null) { 219 | val next = take(serial) 220 | if (next != null) { 221 | if (next.remainingDelay != 0L) { 222 | /* the delay may not have elapsed yet */ 223 | next.remainingDelay = Math.max(0L, targetTimeMillis - System.currentTimeMillis()) 224 | } 225 | /* a task having the same serial was queued, execute it */ 226 | execute(next) 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | 234 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.utils 25 | 26 | import android.annotation.SuppressLint 27 | import android.content.ContentUris 28 | import android.content.Context 29 | import android.database.Cursor 30 | import android.net.Uri 31 | import android.os.Build 32 | import android.os.Environment 33 | import android.provider.DocumentsContract 34 | import android.provider.MediaStore 35 | 36 | import java.io.File 37 | 38 | object FileUtils { 39 | /** 40 | * Get a file path from a Uri. This will get the the path for Storage Access 41 | * Framework Documents, as well as the _data field for the MediaStore and 42 | * other file-based ContentProviders.

43 | *

44 | * Callers should check whether the path is local before assuming it 45 | * represents a local file. 46 | * 47 | * @param context The context. 48 | * @param uri The Uri to query. 49 | * @author paulburke 50 | */ 51 | @SuppressLint("NewApi") 52 | fun getPath(context: Context, uri: Uri): String? { 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) { 54 | when { 55 | isExternalStorageDocument(uri) -> { 56 | // DocumentProvider 57 | val docId = DocumentsContract.getDocumentId(uri) 58 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 59 | val type = split[0] 60 | if ("primary".equals(type, ignoreCase = true)) { 61 | return Environment.getExternalStorageDirectory().toString() + "/" + split[1] 62 | } 63 | return null 64 | // TODO handle non-primary volumes 65 | } 66 | isDownloadsDocument(uri) -> { 67 | // DownloadsProvider 68 | val id = DocumentsContract.getDocumentId(uri) 69 | try { 70 | val contentUri = ContentUris.withAppendedId( 71 | Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id)) 72 | return getDataColumn(context, contentUri, null, null) 73 | } catch (e: NumberFormatException) { 74 | if (id.startsWith("raw:/")) { 75 | val newPath = id.substring("raw:/".length) 76 | val exists = File(newPath).exists() 77 | if (exists) 78 | return newPath 79 | } 80 | } 81 | } 82 | isMediaDocument(uri) -> { 83 | // MediaProvider 84 | val docId = DocumentsContract.getDocumentId(uri) 85 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 86 | val type = split[0] 87 | var contentUri: Uri? = null 88 | when (type) { 89 | "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 90 | "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI 91 | "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 92 | } 93 | val selection = "_id=?" 94 | val selectionArgs = arrayOf(split[1]) 95 | return getDataColumn(context, contentUri, selection, selectionArgs) 96 | } 97 | } 98 | } 99 | return when { 100 | "content".equals(uri.scheme!!, ignoreCase = true) -> // MediaStore (and general) 101 | // Return the remote address 102 | if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) 103 | "file".equals(uri.scheme!!, ignoreCase = true) -> // File 104 | uri.path 105 | else -> null 106 | } 107 | } 108 | 109 | /** 110 | * Get the value of the data column for this Uri. This is useful for 111 | * MediaStore Uris, and other file-based ContentProviders. 112 | * 113 | * @param context The context. 114 | * @param uri The Uri to query. 115 | * @param selection (Optional) Filter used in the query. 116 | * @param selectionArgs (Optional) Selection arguments used in the query. 117 | * @return The value of the _data column, which is typically a file path. 118 | * @author paulburke 119 | */ 120 | private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array?): String? { 121 | var cursor: Cursor? = null 122 | val column = "_data" 123 | val projection = arrayOf(column) 124 | try { 125 | cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) 126 | if (cursor != null && cursor.moveToFirst()) { 127 | val columnIndex = cursor.getColumnIndexOrThrow(column) 128 | return cursor.getString(columnIndex) 129 | } 130 | } finally { 131 | cursor?.close() 132 | } 133 | return null 134 | } 135 | 136 | /** 137 | * @param uri The Uri to check. 138 | * @return Whether the Uri authority is Google Photos. 139 | */ 140 | private fun isGooglePhotosUri(uri: Uri): Boolean { 141 | return "com.google.android.apps.photos.content" == uri.authority 142 | } 143 | 144 | /** 145 | * @param uri The Uri to check. 146 | * @return Whether the Uri authority is ExternalStorageProvider. 147 | */ 148 | private fun isExternalStorageDocument(uri: Uri): Boolean { 149 | return "com.android.externalstorage.documents" == uri.authority 150 | } 151 | 152 | /** 153 | * @param uri The Uri to check. 154 | * @return Whether the Uri authority is DownloadsProvider. 155 | */ 156 | private fun isDownloadsDocument(uri: Uri): Boolean { 157 | return "com.android.providers.downloads.documents" == uri.authority 158 | } 159 | 160 | /** 161 | * @param uri The Uri to check. 162 | * @return Whether the Uri authority is MediaProvider. 163 | */ 164 | private fun isMediaDocument(uri: Uri): Boolean { 165 | return "com.android.providers.media.documents" == uri.authority 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/utils/TrimVideoUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.utils 25 | 26 | import android.content.Context 27 | import android.media.* 28 | import android.net.Uri 29 | import android.os.Handler 30 | import android.os.Looper 31 | import android.util.SparseIntArray 32 | import androidx.annotation.NonNull 33 | import androidx.annotation.WorkerThread 34 | import com.googlecode.mp4parser.FileDataSourceViaHeapImpl 35 | import com.googlecode.mp4parser.authoring.Track 36 | import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder 37 | import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator 38 | import com.googlecode.mp4parser.authoring.tracks.AppendTrack 39 | import com.googlecode.mp4parser.authoring.tracks.CroppedTrack 40 | import com.lb.video_trimmer_library.interfaces.VideoTrimmingListener 41 | import java.io.File 42 | import java.io.FileOutputStream 43 | import java.io.IOException 44 | import java.nio.ByteBuffer 45 | import java.util.* 46 | 47 | object TrimVideoUtils { 48 | private const val DEFAULT_BUFFER_SIZE = 1024 * 1024 49 | 50 | @JvmStatic 51 | @WorkerThread 52 | fun startTrim(context: Context, inputVideoUri: Uri, outputTrimmedVideoFile: File, startMs: Long, endMs: Long, durationInMs: Long, callback: VideoTrimmingListener) { 53 | // Log.d("AppLog", "startTrim") 54 | outputTrimmedVideoFile.parentFile.mkdirs() 55 | outputTrimmedVideoFile.delete() 56 | var succeeded = false 57 | if (startMs <= 0L && endMs >= durationInMs) { 58 | // Log.d("AppLog", "trimmed file is the entire video, so just copy it") 59 | context.contentResolver.openInputStream(inputVideoUri).use { 60 | it?.copyTo(FileOutputStream(outputTrimmedVideoFile)) 61 | succeeded = it != null && outputTrimmedVideoFile.exists() 62 | } 63 | // Log.d("AppLog", "succeeded copying?$succeeded") 64 | } 65 | if (!succeeded) { 66 | // Log.d("AppLog", "trying to trim using mp4parser...") 67 | try { 68 | val inputFilePath = FileUtils.getPath(context, inputVideoUri) 69 | succeeded = genVideoUsingMp4Parser(inputFilePath, outputTrimmedVideoFile, startMs, endMs) 70 | } catch (e: Exception) { 71 | } 72 | // Log.d("AppLog", "succeeded using mp4parser? $succeeded") 73 | } 74 | if (!succeeded) { 75 | // Log.d("AppLog", "trying to trim using Android framework API...") 76 | succeeded = genVideoUsingMuxer(context, inputVideoUri, outputTrimmedVideoFile.absolutePath, startMs, endMs, true, true) 77 | // Log.d("AppLog", "succeeded trimming using Android framework API?$succeeded") 78 | } 79 | Handler(Looper.getMainLooper()).post { 80 | // callback.onFinishedTrimming(if (succeeded) Uri.parse(outputTrimmedVideoFile.toString()) else null) 81 | callback.onFinishedTrimming(if (succeeded) Uri.parse(outputTrimmedVideoFile.absolutePath) else null) 82 | } 83 | } 84 | 85 | @Throws(IOException::class) 86 | private fun genVideoUsingMp4Parser(filePath: String?, dst: File, startMs: Long, endMs: Long): Boolean { 87 | if (filePath.isNullOrBlank() || !File(filePath).exists()) 88 | return false 89 | // NOTE: Switched to using FileDataSourceViaHeapImpl since it does not use memory mapping (VM). 90 | // Otherwise we get OOM with large movie files. 91 | val movie = MovieCreator.build(FileDataSourceViaHeapImpl(filePath)) 92 | val tracks = movie.tracks 93 | movie.tracks = LinkedList() 94 | // remove all tracks we will create new tracks from the old 95 | var startTime1 = (startMs / 1000).toDouble() 96 | var endTime1 = (endMs / 1000).toDouble() 97 | var timeCorrected = false 98 | // Here we try to find a track that has sync samples. Since we can only start decoding 99 | // at such a sample we SHOULD make sure that the start of the new fragment is exactly 100 | // such a frame 101 | for (track in tracks) { 102 | if (track.syncSamples != null && track.syncSamples.isNotEmpty()) { 103 | if (timeCorrected) { 104 | // This exception here could be a false positive in case we have multiple tracks 105 | // with sync samples at exactly the same positions. E.g. a single movie containing 106 | // multiple qualities of the same video (Microsoft Smooth Streaming file) 107 | // throw RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported.") 108 | return false 109 | } 110 | startTime1 = correctTimeToSyncSample(track, startTime1, false) 111 | endTime1 = correctTimeToSyncSample(track, endTime1, true) 112 | timeCorrected = true 113 | } 114 | } 115 | for (track in tracks) { 116 | var currentSample: Long = 0 117 | var currentTime = 0.0 118 | var lastTime = -1.0 119 | var startSample1: Long = -1 120 | var endSample1: Long = -1 121 | for (i in 0 until track.sampleDurations.size) { 122 | val delta = track.sampleDurations[i] 123 | if (currentTime > lastTime && currentTime <= startTime1) { 124 | // current sample is still before the new starttime 125 | startSample1 = currentSample 126 | } 127 | if (currentTime > lastTime && currentTime <= endTime1) { 128 | // current sample is after the new start time and still before the new endtime 129 | endSample1 = currentSample 130 | } 131 | lastTime = currentTime 132 | currentTime += delta.toDouble() / track.trackMetaData.timescale.toDouble() 133 | ++currentSample 134 | } 135 | movie.addTrack(AppendTrack(CroppedTrack(track, startSample1, endSample1))) 136 | } 137 | dst.parentFile.mkdirs() 138 | if (!dst.exists()) { 139 | dst.createNewFile() 140 | } 141 | val out = DefaultMp4Builder().build(movie) 142 | val fos = FileOutputStream(dst) 143 | val fc = fos.channel 144 | out.writeContainer(fc) 145 | fc.close() 146 | fos.close() 147 | return true 148 | } 149 | 150 | //https://stackoverflow.com/a/44653626/878126 https://android.googlesource.com/platform/packages/apps/Gallery2/+/634248d/src/com/android/gallery3d/app/VideoUtils.java 151 | @JvmStatic 152 | @WorkerThread 153 | private fun genVideoUsingMuxer(context: Context, uri: Uri, dstPath: String, startMs: Long, endMs: Long, useAudio: Boolean, useVideo: Boolean): Boolean { 154 | // Set up MediaExtractor to read from the source. 155 | val extractor = MediaExtractor() 156 | // val isRawResId=uri.scheme == "android.resource" && uri.host == context.packageName && !uri.pathSegments.isNullOrEmpty()) 157 | val fileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")!!.fileDescriptor 158 | extractor.setDataSource(fileDescriptor) 159 | val trackCount = extractor.trackCount 160 | // Set up MediaMuxer for the destination. 161 | val muxer = MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) 162 | // Set up the tracks and retrieve the max buffer size for selected tracks. 163 | val indexMap = SparseIntArray(trackCount) 164 | var bufferSize = -1 165 | try { 166 | for (i in 0 until trackCount) { 167 | val format = extractor.getTrackFormat(i) 168 | val mime = format.getString(MediaFormat.KEY_MIME) 169 | var selectCurrentTrack = false 170 | if (mime.startsWith("audio/") && useAudio) { 171 | selectCurrentTrack = true 172 | } else if (mime.startsWith("video/") && useVideo) { 173 | selectCurrentTrack = true 174 | } 175 | if (selectCurrentTrack) { 176 | extractor.selectTrack(i) 177 | val dstIndex = muxer.addTrack(format) 178 | indexMap.put(i, dstIndex) 179 | if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { 180 | val newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) 181 | bufferSize = if (newSize > bufferSize) newSize else bufferSize 182 | } 183 | } 184 | } 185 | if (bufferSize < 0) 186 | bufferSize = DEFAULT_BUFFER_SIZE 187 | // Set up the orientation and starting time for extractor. 188 | val retrieverSrc = MediaMetadataRetriever() 189 | retrieverSrc.setDataSource(fileDescriptor) 190 | val degreesString = retrieverSrc.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) 191 | if (degreesString != null) { 192 | val degrees = Integer.parseInt(degreesString) 193 | if (degrees >= 0) 194 | muxer.setOrientationHint(degrees) 195 | } 196 | if (startMs > 0) 197 | extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC) 198 | // Copy the samples from MediaExtractor to MediaMuxer. We will loop 199 | // for copying each sample and stop when we get to the end of the source 200 | // file or exceed the end time of the trimming. 201 | val offset = 0 202 | var trackIndex: Int 203 | val dstBuf = ByteBuffer.allocate(bufferSize) 204 | val bufferInfo = MediaCodec.BufferInfo() 205 | // try { 206 | muxer.start() 207 | while (true) { 208 | bufferInfo.offset = offset 209 | bufferInfo.size = extractor.readSampleData(dstBuf, offset) 210 | if (bufferInfo.size < 0) { 211 | //InstabugSDKLogger.d(TAG, "Saw input EOS."); 212 | bufferInfo.size = 0 213 | break 214 | } else { 215 | bufferInfo.presentationTimeUs = extractor.sampleTime 216 | if (endMs > 0 && bufferInfo.presentationTimeUs > endMs * 1000) { 217 | //InstabugSDKLogger.d(TAG, "The current sample is over the trim end time."); 218 | break 219 | } else { 220 | bufferInfo.flags = extractor.sampleFlags 221 | trackIndex = extractor.sampleTrackIndex 222 | muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, 223 | bufferInfo) 224 | extractor.advance() 225 | } 226 | } 227 | } 228 | muxer.stop() 229 | return true 230 | // } catch (e: IllegalStateException) { 231 | // Swallow the exception due to malformed source. 232 | //InstabugSDKLogger.w(TAG, "The source video file is malformed"); 233 | } catch (e: Exception) { 234 | e.printStackTrace() 235 | } finally { 236 | muxer.release() 237 | } 238 | return false 239 | } 240 | 241 | 242 | private fun correctTimeToSyncSample(@NonNull track: Track, cutHere: Double, next: Boolean): Double { 243 | val timeOfSyncSamples = DoubleArray(track.syncSamples.size) 244 | var currentSample: Long = 0 245 | var currentTime = 0.0 246 | for (i in 0 until track.sampleDurations.size) { 247 | val delta = track.sampleDurations[i] 248 | if (Arrays.binarySearch(track.syncSamples, currentSample + 1) >= 0) { 249 | // samples always start with 1 but we start with zero therefore +1 250 | timeOfSyncSamples[Arrays.binarySearch(track.syncSamples, currentSample + 1)] = currentTime 251 | } 252 | currentTime += delta.toDouble() / track.trackMetaData.timescale.toDouble() 253 | ++currentSample 254 | } 255 | var previous = 0.0 256 | for (timeOfSyncSample in timeOfSyncSamples) { 257 | if (timeOfSyncSample > cutHere) { 258 | return if (next) { 259 | timeOfSyncSample 260 | } else { 261 | previous 262 | } 263 | } 264 | previous = timeOfSyncSample 265 | } 266 | return timeOfSyncSamples[timeOfSyncSamples.size - 1] 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/utils/UiThreadExecutor.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2010-2016 eBusiness Information, Excilys Group 3 | * 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | * use this file except in compliance with the License. You may obtain a copy of 7 | * the License at 8 | * 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * 13 | * Unless required by applicable law or agreed To in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | * License for the specific language governing permissions and limitations under 17 | * the License. 18 | */ 19 | package com.lb.video_trimmer_library.utils 20 | 21 | import android.os.Handler 22 | import android.os.Looper 23 | import android.os.Message 24 | import android.os.SystemClock 25 | import java.util.* 26 | 27 | /** 28 | * This class provide operations for 29 | * UiThread tasks. 30 | */ 31 | internal object UiThreadExecutor { 32 | private val HANDLER = object : Handler(Looper.getMainLooper()) { 33 | override fun handleMessage(msg: Message) { 34 | val callback = msg.callback 35 | if (callback != null) { 36 | callback.run() 37 | decrementToken(msg.obj as Token) 38 | } else { 39 | super.handleMessage(msg) 40 | } 41 | } 42 | } 43 | 44 | private val TOKENS = HashMap() 45 | 46 | /** 47 | * Store a new task in the map for providing cancellation. This method is 48 | * used by AndroidAnnotations and not intended to be called by clients. 49 | * 50 | * @param id the identifier of the task 51 | * @param task the task itself 52 | * @param delay the delay or zero to run immediately 53 | */ 54 | fun runTask(id: String, task: Runnable, delay: Long) { 55 | if ("" == id) { 56 | HANDLER.postDelayed(task, delay) 57 | return 58 | } 59 | val time = SystemClock.uptimeMillis() + delay 60 | HANDLER.postAtTime(task, nextToken(id), time) 61 | } 62 | 63 | private fun nextToken(id: String): Token { 64 | synchronized(TOKENS) { 65 | var token = TOKENS[id] 66 | if (token == null) { 67 | token = Token(id) 68 | TOKENS[id] = token 69 | } 70 | ++token.runnablesCount 71 | return token 72 | } 73 | } 74 | 75 | private fun decrementToken(token: Token) { 76 | synchronized(TOKENS) { 77 | if (--token.runnablesCount == 0) { 78 | val id = token.id 79 | val old = TOKENS.remove(id) 80 | if (old != token && old != null) { 81 | // a runnable finished after cancelling, we just removed a 82 | // wrong token, lets put it back 83 | TOKENS[id] = old 84 | } 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Cancel all tasks having the specified `id`. 91 | * 92 | * @param id the cancellation identifier 93 | */ 94 | fun cancelAll(id: String) { 95 | val token: Token? 96 | synchronized(TOKENS) { 97 | token = TOKENS.remove(id) 98 | } 99 | if (token == null) { 100 | // nothing to cancel 101 | return 102 | } 103 | HANDLER.removeCallbacksAndMessages(token) 104 | } 105 | 106 | // should not be instantiated 107 | private class Token constructor(internal val id: String) { 108 | internal var runnablesCount = 0 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/view/RangeSeekBarView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.view 25 | 26 | import android.content.Context 27 | import android.graphics.Canvas 28 | import android.graphics.Paint 29 | import android.util.AttributeSet 30 | import android.util.TypedValue 31 | import android.view.MotionEvent 32 | import android.view.View 33 | import androidx.annotation.ColorInt 34 | import com.lb.video_trimmer_library.interfaces.OnRangeSeekBarListener 35 | import kotlin.math.absoluteValue 36 | 37 | @Suppress("LeakingThis") 38 | open class RangeSeekBarView @JvmOverloads constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : 39 | View(context, attrs, defStyleAttr) { 40 | enum class ThumbType(val index: kotlin.Int) { 41 | LEFT(0), RIGHT(1) 42 | } 43 | 44 | @Suppress("MemberVisibilityCanBePrivate") 45 | private val thumbTouchExtraMultiplier = initThumbTouchExtraMultiplier() 46 | private val thumbs = arrayOf(Thumb(ThumbType.LEFT.index), Thumb(ThumbType.RIGHT.index)) 47 | private var listeners = HashSet() 48 | private var maxWidth: Float = 0.toFloat() 49 | val thumbWidth = initThumbWidth(context) 50 | private var viewWidth: Int = 0 51 | private var pixelRangeMin: Float = 0.toFloat() 52 | private var pixelRangeMax: Float = 0.toFloat() 53 | private val scaleRangeMax: Float = 100f 54 | private var firstRun: Boolean = true 55 | private val shadowPaint = Paint() 56 | private val strokePaint = Paint() 57 | private val edgePaint = Paint() 58 | private var currentThumb = ThumbType.LEFT.index 59 | 60 | 61 | init { 62 | isFocusable = true 63 | isFocusableInTouchMode = true 64 | shadowPaint.isAntiAlias = true 65 | shadowPaint.color = initShadowColor() 66 | strokePaint.isAntiAlias = true 67 | strokePaint.style = Paint.Style.STROKE 68 | strokePaint.strokeWidth = 69 | TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, context.resources.displayMetrics) 70 | strokePaint.color = 0xffffffff.toInt() 71 | edgePaint.isAntiAlias = true 72 | edgePaint.color = 0xffffffff.toInt() 73 | } 74 | 75 | @ColorInt 76 | open fun initShadowColor(): Int = 0xB1000000.toInt() 77 | 78 | open fun initThumbTouchExtraMultiplier() = 1.0f 79 | 80 | open fun initThumbWidth(context: Context) = 81 | TypedValue.applyDimension( 82 | TypedValue.COMPLEX_UNIT_DIP, 83 | 27f, 84 | context.resources.displayMetrics 85 | ).toInt().coerceAtLeast(1) 86 | 87 | fun initMaxWidth() { 88 | maxWidth = thumbs[ThumbType.RIGHT.index].pos - thumbs[ThumbType.LEFT.index].pos 89 | onSeekStop(this, ThumbType.LEFT.index, thumbs[ThumbType.LEFT.index].value) 90 | onSeekStop(this, ThumbType.RIGHT.index, thumbs[ThumbType.RIGHT.index].value) 91 | } 92 | 93 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 94 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 95 | viewWidth=measuredWidth 96 | pixelRangeMin = 0f 97 | pixelRangeMax = (viewWidth - thumbWidth).toFloat() 98 | if (firstRun) { 99 | for ((index, thumb) in thumbs.withIndex()) { 100 | thumb.value = scaleRangeMax * index 101 | thumb.pos = pixelRangeMax * index 102 | } 103 | // Fire listener callback 104 | onCreate(this, currentThumb, getThumbValue(currentThumb)) 105 | firstRun = false 106 | } 107 | } 108 | 109 | override fun onDraw(canvas: Canvas) { 110 | super.onDraw(canvas) 111 | if (thumbs.isEmpty()) 112 | return 113 | // draw shadows outside of selected range 114 | for (thumb in thumbs) { 115 | if (thumb.index == ThumbType.LEFT.index) { 116 | val x = thumb.pos + paddingLeft 117 | if (x > pixelRangeMin) 118 | canvas.drawRect(thumbWidth.toFloat(), 0f, (x + thumbWidth), height.toFloat(), shadowPaint) 119 | } else { 120 | val x = thumb.pos - paddingRight 121 | if (x < pixelRangeMax) 122 | canvas.drawRect(x, 0f, (viewWidth - thumbWidth).toFloat(), height.toFloat(), shadowPaint) 123 | } 124 | } 125 | //draw stroke around selected range 126 | canvas.drawRect( 127 | (thumbs[ThumbType.LEFT.index].pos + paddingLeft + thumbWidth), 128 | 0f, 129 | thumbs[ThumbType.RIGHT.index].pos - paddingRight, 130 | height.toFloat(), 131 | strokePaint 132 | ) 133 | //draw edges 134 | val circleRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6f, context.resources.displayMetrics) 135 | canvas.drawCircle( 136 | (thumbs[ThumbType.LEFT.index].pos + paddingLeft + thumbWidth), 137 | height.toFloat() / 2f, 138 | circleRadius, 139 | edgePaint 140 | ) 141 | canvas.drawCircle( 142 | thumbs[ThumbType.RIGHT.index].pos - paddingRight, 143 | height.toFloat() / 2f, 144 | circleRadius, 145 | edgePaint 146 | ) 147 | } 148 | 149 | override fun onTouchEvent(ev: MotionEvent): Boolean { 150 | val mThumb: Thumb 151 | val mThumb2: Thumb 152 | val coordinate = ev.x 153 | val action = ev.action 154 | when (action) { 155 | MotionEvent.ACTION_DOWN -> { 156 | // Remember where we started 157 | currentThumb = getClosestThumb(coordinate) 158 | if (currentThumb == -1) 159 | return false 160 | mThumb = thumbs[currentThumb] 161 | mThumb.lastTouchX = coordinate 162 | onSeekStart(this, currentThumb, mThumb.value) 163 | return true 164 | } 165 | MotionEvent.ACTION_UP -> { 166 | if (currentThumb == -1) 167 | return false 168 | mThumb = thumbs[currentThumb] 169 | onSeekStop(this, currentThumb, mThumb.value) 170 | return true 171 | } 172 | MotionEvent.ACTION_MOVE -> { 173 | mThumb = thumbs[currentThumb] 174 | mThumb2 = 175 | thumbs[if (currentThumb == ThumbType.LEFT.index) ThumbType.RIGHT.index else ThumbType.LEFT.index] 176 | // Calculate the distance moved 177 | val dx = coordinate - mThumb.lastTouchX 178 | val newX = mThumb.pos + dx 179 | when { 180 | currentThumb == 0 -> when { 181 | newX + thumbWidth >= mThumb2.pos -> mThumb.pos = mThumb2.pos - thumbWidth 182 | newX <= pixelRangeMin -> mThumb.pos = pixelRangeMin 183 | else -> { 184 | //Check if thumb is not out of max width 185 | checkPositionThumb(mThumb, mThumb2, dx, true) 186 | // Move the object 187 | mThumb.pos = mThumb.pos + dx 188 | // Remember this touch position for the next move event 189 | mThumb.lastTouchX = coordinate 190 | } 191 | } 192 | newX <= mThumb2.pos + thumbWidth -> mThumb.pos = mThumb2.pos + thumbWidth 193 | newX >= pixelRangeMax -> mThumb.pos = pixelRangeMax 194 | else -> { 195 | //Check if thumb is not out of max width 196 | checkPositionThumb(mThumb2, mThumb, dx, false) 197 | // Move the object 198 | mThumb.pos = mThumb.pos + dx 199 | // Remember this touch position for the next move event 200 | mThumb.lastTouchX = coordinate 201 | } 202 | } 203 | setThumbPos(currentThumb, mThumb.pos) 204 | // Invalidate to request a redraw 205 | invalidate() 206 | return true 207 | } 208 | } 209 | return false 210 | } 211 | 212 | private fun checkPositionThumb(thumbLeft: Thumb, thumbRight: Thumb, dx: Float, isLeftMove: Boolean) { 213 | if (isLeftMove && dx < 0) { 214 | if (thumbRight.pos - (thumbLeft.pos + dx) > maxWidth) { 215 | thumbRight.pos = thumbLeft.pos + dx + maxWidth 216 | setThumbPos(ThumbType.RIGHT.index, thumbRight.pos) 217 | } 218 | } else if (!isLeftMove && dx > 0) { 219 | if (thumbRight.pos + dx - thumbLeft.pos > maxWidth) { 220 | thumbLeft.pos = thumbRight.pos + dx - maxWidth 221 | setThumbPos(ThumbType.LEFT.index, thumbLeft.pos) 222 | } 223 | } 224 | } 225 | 226 | private fun pixelToScale(index: Int, pixelValue: Float): Float { 227 | val scale = pixelValue * 100 / pixelRangeMax 228 | return if (index == 0) { 229 | val pxThumb = scale * thumbWidth / 100 230 | scale + pxThumb * 100 / pixelRangeMax 231 | } else { 232 | val pxThumb = (100 - scale) * thumbWidth / 100 233 | scale - pxThumb * 100 / pixelRangeMax 234 | } 235 | } 236 | 237 | private fun scaleToPixel(index: Int, scaleValue: Float): Float { 238 | val px = scaleValue * pixelRangeMax / 100 239 | return if (index == 0) { 240 | val pxThumb = scaleValue * thumbWidth / 100 241 | px - pxThumb 242 | } else { 243 | val pxThumb = (100 - scaleValue) * thumbWidth / 100 244 | px + pxThumb 245 | } 246 | } 247 | 248 | private fun calculateThumbValue(index: Int) { 249 | if (index < thumbs.size && !thumbs.isEmpty()) { 250 | val th = thumbs[index] 251 | th.value = pixelToScale(index, th.pos) 252 | onSeek(this, index, th.value) 253 | } 254 | } 255 | 256 | private fun calculateThumbPos(index: Int) { 257 | if (index < thumbs.size && !thumbs.isEmpty()) { 258 | val th = thumbs[index] 259 | th.pos = scaleToPixel(index, th.value) 260 | } 261 | } 262 | 263 | private fun getThumbValue(index: Int): Float { 264 | return thumbs[index].value 265 | } 266 | 267 | fun setThumbValue(index: Int, value: Float) { 268 | thumbs[index].value = value 269 | calculateThumbPos(index) 270 | // Tell the view we want a complete redraw 271 | invalidate() 272 | } 273 | 274 | private fun setThumbPos(index: Int, pos: Float) { 275 | thumbs[index].pos = pos 276 | calculateThumbValue(index) 277 | // Tell the view we want a complete redraw 278 | invalidate() 279 | } 280 | 281 | private fun getClosestThumb(xPos: Float): Int { 282 | if (thumbs.isEmpty()) 283 | return -1 284 | var closest = -1 285 | var minDistanceFound = Float.MAX_VALUE 286 | val x = xPos - thumbWidth//+ paddingLeft 287 | // Log.d("AppLog", "xPos:$xPos -> x: $x") 288 | for (thumb in thumbs) { 289 | val thumbPos = if (thumb.index == ThumbType.LEFT.index) thumb.pos else thumb.pos - thumbWidth 290 | // Log.d("AppLog", "thumb ${thumb.index} pos: $thumbPos") 291 | // Find thumb closest to x coordinate 292 | val xMin = thumbPos - thumbWidth * thumbTouchExtraMultiplier 293 | val xMax = thumbPos + thumbWidth * thumbTouchExtraMultiplier 294 | if (x in xMin..xMax) { 295 | val distance = (thumbPos - x).absoluteValue 296 | if (distance < minDistanceFound) { 297 | closest = thumb.index 298 | // Log.d("AppLog", "x: $x distance: $distance selectedThumb:$closest") 299 | minDistanceFound = distance 300 | } 301 | } 302 | } 303 | return closest 304 | } 305 | 306 | fun addOnRangeSeekBarListener(listener: OnRangeSeekBarListener) { 307 | listeners.add(listener) 308 | } 309 | 310 | private fun onCreate(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 311 | listeners.forEach { item -> item.onCreate(rangeSeekBarView, index, value) } 312 | } 313 | 314 | private fun onSeek(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 315 | listeners.forEach { item -> item.onSeek(rangeSeekBarView, index, value) } 316 | } 317 | 318 | private fun onSeekStart(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 319 | listeners.forEach { item -> item.onSeekStart(rangeSeekBarView, index, value) } 320 | } 321 | 322 | private fun onSeekStop(rangeSeekBarView: RangeSeekBarView, index: Int, value: Float) { 323 | listeners.forEach { item -> item.onSeekStop(rangeSeekBarView, index, value) } 324 | } 325 | 326 | class Thumb(val index: Int = 0) { 327 | var value: Float = 0f 328 | var pos: Float = 0f 329 | var lastTouchX: Float = 0f 330 | } 331 | 332 | } 333 | -------------------------------------------------------------------------------- /video_trimmer_library/src/main/java/com/lb/video_trimmer_library/view/TimeLineView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Knowledge, education for life. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | package com.lb.video_trimmer_library.view 25 | 26 | import android.annotation.SuppressLint 27 | import android.content.Context 28 | import android.graphics.Bitmap 29 | import android.graphics.BitmapFactory 30 | import android.graphics.Canvas 31 | import android.media.MediaMetadataRetriever 32 | import android.media.ThumbnailUtils 33 | import android.net.Uri 34 | import android.os.Build.VERSION 35 | import android.os.Build.VERSION_CODES 36 | import android.util.AttributeSet 37 | import android.view.View 38 | import com.lb.video_trimmer_library.utils.BackgroundExecutor 39 | import com.lb.video_trimmer_library.utils.UiThreadExecutor 40 | 41 | open class TimeLineView @JvmOverloads constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : 42 | View(context, attrs, defStyleAttr) { 43 | private var videoUri: Uri? = null 44 | @Suppress("LeakingThis") 45 | // private var bitmapList: LongSparseArray? = null 46 | private val bitmapList = ArrayList() 47 | 48 | override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) { 49 | super.onSizeChanged(w, h, oldW, oldH) 50 | if (w != oldW) 51 | getBitmap(w, h) 52 | } 53 | 54 | private fun getBitmap(viewWidth: Int, viewHeight: Int) { 55 | // Set thumbnail properties (Thumbs are squares) 56 | @Suppress("UnnecessaryVariable") 57 | val thumbSize = viewHeight 58 | val numThumbs = Math.ceil((viewWidth.toFloat() / thumbSize).toDouble()).toInt() 59 | bitmapList.clear() 60 | if (isInEditMode) { 61 | val bitmap = ThumbnailUtils.extractThumbnail( 62 | BitmapFactory.decodeResource(resources, android.R.drawable.sym_def_app_icon)!!, thumbSize, thumbSize 63 | ) 64 | for (i in 0 until numThumbs) 65 | bitmapList.add(bitmap) 66 | return 67 | } 68 | BackgroundExecutor.cancelAll("", true) 69 | BackgroundExecutor.execute(object : BackgroundExecutor.Task("", 0L, "") { 70 | override fun execute() { 71 | try { 72 | val thumbnailList = ArrayList() 73 | val mediaMetadataRetriever = MediaMetadataRetriever() 74 | mediaMetadataRetriever.setDataSource(context, videoUri) 75 | // Retrieve media data 76 | val videoLengthInMs = 77 | mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong() * 1000L 78 | val interval = videoLengthInMs / numThumbs 79 | for (i in 0 until numThumbs) { 80 | var bitmap: Bitmap? = if (VERSION.SDK_INT >= VERSION_CODES.O_MR1) 81 | mediaMetadataRetriever.getScaledFrameAtTime( 82 | i * interval, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, thumbSize, thumbSize 83 | ) 84 | else mediaMetadataRetriever.getFrameAtTime( 85 | i * interval, 86 | MediaMetadataRetriever.OPTION_CLOSEST_SYNC 87 | ) 88 | if (bitmap != null) 89 | bitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbSize, thumbSize) 90 | thumbnailList.add(bitmap) 91 | } 92 | mediaMetadataRetriever.release() 93 | returnBitmaps(thumbnailList) 94 | } catch (e: Throwable) { 95 | Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e) 96 | } 97 | 98 | } 99 | } 100 | ) 101 | } 102 | 103 | private fun returnBitmaps(thumbnailList: ArrayList) { 104 | UiThreadExecutor.runTask("", Runnable { 105 | bitmapList.clear() 106 | bitmapList.addAll(thumbnailList) 107 | invalidate() 108 | }, 0L) 109 | } 110 | 111 | @SuppressLint("DrawAllocation") 112 | override fun onDraw(canvas: Canvas) { 113 | super.onDraw(canvas) 114 | canvas.save() 115 | var x = 0 116 | val thumbSize = height 117 | for (bitmap in bitmapList) { 118 | if (bitmap != null) 119 | canvas.drawBitmap(bitmap, x.toFloat(), 0f, null) 120 | x += thumbSize 121 | } 122 | } 123 | 124 | fun setVideo(data: Uri) { 125 | videoUri = data 126 | } 127 | } 128 | --------------------------------------------------------------------------------