├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------