react-native-video-trim 7.1.1 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -1
- package/android/src/main/java/com/videotrim/BaseVideoTrimModule.kt +488 -34
- package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +95 -36
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +38 -16
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +77 -0
- package/android/src/main/res/drawable/speaker_slash_fill.xml +19 -0
- package/android/src/main/res/drawable/speaker_wave_2_fill.xml +23 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +22 -0
- package/android/src/newarch/VideoTrimModule.kt +33 -0
- package/android/src/oldarch/VideoTrimModule.kt +41 -0
- package/android/src/oldarch/VideoTrimSpec.kt +17 -0
- package/ios/VideoTrim.mm +155 -1
- package/ios/VideoTrim.swift +632 -39
- package/ios/VideoTrimmerViewController.swift +96 -3
- package/lib/module/NativeVideoTrim.js +52 -0
- package/lib/module/NativeVideoTrim.js.map +1 -1
- package/lib/module/index.js +142 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts +148 -0
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +62 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeVideoTrim.ts +161 -0
- package/src/index.tsx +183 -0
|
@@ -25,23 +25,39 @@ object StorageUtil {
|
|
|
25
25
|
return file.absolutePath
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Headless API outputs (compress, toGif, merge, extractAudio, getFrameAt) go to the
|
|
29
|
+
// cache directory. The OS may purge these files under storage pressure when the app is
|
|
30
|
+
// not running, which avoids unbounded storage growth from repeated headless operations.
|
|
31
|
+
fun getCacheOutputPath(context: Context, outputExt: String): String {
|
|
32
|
+
val timestamp = System.currentTimeMillis() / 1000
|
|
33
|
+
val file = File(context.cacheDir, "${VideoTrimmerUtil.FILE_PREFIX}_${timestamp}.$outputExt")
|
|
34
|
+
return file.absolutePath
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
fun isFileExists(filePath: String?): Boolean {
|
|
29
38
|
if (TextUtils.isEmpty(filePath)) return false
|
|
30
39
|
return File(filePath!!).exists()
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
// Scans both the persistent directory (showEditor/trim outputs) and the cache
|
|
43
|
+
// directory (headless API outputs) for files matching our prefix.
|
|
33
44
|
fun listFiles(context: Context): Array<String> {
|
|
34
|
-
val
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
val dirs = listOf(context.filesDir, context.cacheDir)
|
|
46
|
+
return dirs.flatMap { dir ->
|
|
47
|
+
dir.listFiles { _, name -> name.startsWith(VideoTrimmerUtil.FILE_PREFIX) }?.toList() ?: emptyList()
|
|
48
|
+
}.map { it.absolutePath }.toTypedArray()
|
|
38
49
|
}
|
|
39
50
|
|
|
51
|
+
// Path-restricted delete: only allows deletion of files inside our filesDir or cacheDir
|
|
52
|
+
// to prevent accidental deletion of arbitrary files on the device.
|
|
40
53
|
fun deleteFile(path: String?): Boolean {
|
|
41
54
|
if (TextUtils.isEmpty(path)) return true
|
|
42
55
|
val file = File(path!!).canonicalFile
|
|
43
|
-
val
|
|
44
|
-
|
|
56
|
+
val allowedDirs = listOf(
|
|
57
|
+
BaseUtils.getContext().filesDir.canonicalFile,
|
|
58
|
+
BaseUtils.getContext().cacheDir.canonicalFile,
|
|
59
|
+
)
|
|
60
|
+
if (allowedDirs.none { file.path.startsWith(it.path) }) return false
|
|
45
61
|
return deleteFile(file)
|
|
46
62
|
}
|
|
47
63
|
|
|
@@ -62,46 +78,89 @@ object StorageUtil {
|
|
|
62
78
|
return file.delete()
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
|
|
66
|
-
fun saveVideoToGallery(context: ReactApplicationContext, videoFilePath: String?) {
|
|
67
|
-
val videoFile = File(videoFilePath!!)
|
|
81
|
+
private val IMAGE_EXTENSIONS = setOf("jpg", "jpeg", "png", "gif", "webp", "bmp", "heic", "heif")
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
fun isImageFile(file: File): Boolean {
|
|
84
|
+
val ext = file.extension.lowercase()
|
|
85
|
+
return ext in IMAGE_EXTENSIONS
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Dispatches to the correct MediaStore collection (Images vs Video) based on file
|
|
89
|
+
// extension. Using the wrong collection causes the Photos app to misidentify the file.
|
|
90
|
+
@Throws(IOException::class)
|
|
91
|
+
fun saveToGallery(context: ReactApplicationContext, filePath: String?) {
|
|
92
|
+
val file = File(filePath!!)
|
|
93
|
+
if (isImageFile(file)) {
|
|
94
|
+
saveImageToGallery(context, file)
|
|
71
95
|
} else {
|
|
72
|
-
|
|
73
|
-
saveVideoUsingTraditionalStorage(context, videoFile)
|
|
74
|
-
} catch (e: IOException) {
|
|
75
|
-
throw RuntimeException(e)
|
|
76
|
-
}
|
|
96
|
+
saveVideoToGallery(context, file)
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
// On Android Q+ (API 29+), uses the IS_PENDING pattern: the MediaStore entry is
|
|
101
|
+
// created as pending (invisible to other apps), the file is copied, then IS_PENDING
|
|
102
|
+
// is cleared to make it visible. This prevents partial files from appearing in the
|
|
103
|
+
// gallery during the write.
|
|
104
|
+
@Throws(IOException::class)
|
|
105
|
+
private fun saveVideoToGallery(context: ReactApplicationContext, videoFile: File) {
|
|
106
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
107
|
+
val values = ContentValues().apply {
|
|
108
|
+
put(MediaStore.Video.Media.DISPLAY_NAME, videoFile.name)
|
|
109
|
+
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
|
|
110
|
+
put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
|
|
111
|
+
put(MediaStore.Video.Media.IS_PENDING, 1)
|
|
112
|
+
}
|
|
113
|
+
val resolver = context.contentResolver
|
|
114
|
+
val uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)
|
|
115
|
+
if (uri != null) {
|
|
116
|
+
resolver.openOutputStream(uri)?.use { os -> copyFile(videoFile, os) }
|
|
117
|
+
values.clear()
|
|
118
|
+
values.put(MediaStore.Video.Media.IS_PENDING, 0)
|
|
119
|
+
resolver.update(uri, values, null, null)
|
|
95
120
|
}
|
|
121
|
+
} else {
|
|
122
|
+
val galleryDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
|
|
123
|
+
val dest = File(galleryDir, videoFile.name)
|
|
124
|
+
copyFile(videoFile, dest)
|
|
125
|
+
MediaScannerConnection.scanFile(context, arrayOf(dest.absolutePath), arrayOf("video/*"), null)
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
128
|
|
|
129
|
+
// Same IS_PENDING pattern as saveVideoToGallery but targets MediaStore.Images.Media
|
|
130
|
+
// and Environment.DIRECTORY_PICTURES. MIME type is inferred from the file extension.
|
|
99
131
|
@Throws(IOException::class)
|
|
100
|
-
private fun
|
|
101
|
-
val
|
|
102
|
-
val
|
|
103
|
-
|
|
104
|
-
|
|
132
|
+
private fun saveImageToGallery(context: ReactApplicationContext, imageFile: File) {
|
|
133
|
+
val ext = imageFile.extension.lowercase()
|
|
134
|
+
val mimeType = when (ext) {
|
|
135
|
+
"png" -> "image/png"
|
|
136
|
+
"gif" -> "image/gif"
|
|
137
|
+
"webp" -> "image/webp"
|
|
138
|
+
"bmp" -> "image/bmp"
|
|
139
|
+
"heic", "heif" -> "image/heic"
|
|
140
|
+
else -> "image/jpeg"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
144
|
+
val values = ContentValues().apply {
|
|
145
|
+
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
|
|
146
|
+
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
|
|
147
|
+
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
|
|
148
|
+
put(MediaStore.Images.Media.IS_PENDING, 1)
|
|
149
|
+
}
|
|
150
|
+
val resolver = context.contentResolver
|
|
151
|
+
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
|
152
|
+
if (uri != null) {
|
|
153
|
+
resolver.openOutputStream(uri)?.use { os -> copyFile(imageFile, os) }
|
|
154
|
+
values.clear()
|
|
155
|
+
values.put(MediaStore.Images.Media.IS_PENDING, 0)
|
|
156
|
+
resolver.update(uri, values, null, null)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
val picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
|
|
160
|
+
val dest = File(picturesDir, imageFile.name)
|
|
161
|
+
copyFile(imageFile, dest)
|
|
162
|
+
MediaScannerConnection.scanFile(context, arrayOf(dest.absolutePath), arrayOf(mimeType), null)
|
|
163
|
+
}
|
|
105
164
|
}
|
|
106
165
|
|
|
107
166
|
@Throws(IOException::class)
|
|
@@ -38,6 +38,21 @@ object VideoTrimmerUtil {
|
|
|
38
38
|
@JvmField val THUMB_WIDTH = UnitConverter.dpToPx(25)
|
|
39
39
|
private const val THUMB_RESOLUTION_RES = 2
|
|
40
40
|
|
|
41
|
+
internal fun buildAtempoChain(speed: Double): String {
|
|
42
|
+
var remaining = speed
|
|
43
|
+
val filters = mutableListOf<String>()
|
|
44
|
+
while (remaining < 0.5) {
|
|
45
|
+
filters.add("atempo=0.5")
|
|
46
|
+
remaining /= 0.5
|
|
47
|
+
}
|
|
48
|
+
while (remaining > 2.0) {
|
|
49
|
+
filters.add("atempo=2.0")
|
|
50
|
+
remaining /= 2.0
|
|
51
|
+
}
|
|
52
|
+
filters.add("atempo=$remaining")
|
|
53
|
+
return filters.joinToString(",")
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
fun trim(
|
|
42
57
|
inputFile: String,
|
|
43
58
|
outputFile: String,
|
|
@@ -51,6 +66,8 @@ object VideoTrimmerUtil {
|
|
|
51
66
|
videoHeight: Int,
|
|
52
67
|
videoBitrate: Long,
|
|
53
68
|
enablePreciseTrimming: Boolean,
|
|
69
|
+
removeAudio: Boolean,
|
|
70
|
+
speed: Double,
|
|
54
71
|
callback: VideoTrimListener
|
|
55
72
|
): FFmpegSession {
|
|
56
73
|
val currentDate = Date()
|
|
@@ -67,9 +84,9 @@ object VideoTrimmerUtil {
|
|
|
67
84
|
|
|
68
85
|
val hasUserTransform = userRotationCount != 0 || userIsFlipped
|
|
69
86
|
// Re-encode is required when: (1) user applied flip/rotate, (2) user cropped, or
|
|
70
|
-
// (3) enablePreciseTrimming is on. In
|
|
71
|
-
// either we need video filters or we need frame-accurate cut points.
|
|
72
|
-
val needsReEncode = hasUserTransform || cropNormalized != null || enablePreciseTrimming
|
|
87
|
+
// (3) enablePreciseTrimming is on, or (4) speed != 1.0. In those cases, -c copy
|
|
88
|
+
// won't work because either we need video filters or we need frame-accurate cut points.
|
|
89
|
+
val needsReEncode = hasUserTransform || cropNormalized != null || enablePreciseTrimming || speed != 1.0
|
|
73
90
|
|
|
74
91
|
if (needsReEncode) {
|
|
75
92
|
val videoFilters = mutableListOf<String>()
|
|
@@ -105,6 +122,10 @@ object VideoTrimmerUtil {
|
|
|
105
122
|
}
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
if (speed != 1.0) {
|
|
126
|
+
videoFilters.add("setpts=${1.0 / speed}*PTS")
|
|
127
|
+
}
|
|
128
|
+
|
|
108
129
|
val filterString = videoFilters.joinToString(",")
|
|
109
130
|
// Preserve source quality by matching the original bitrate. Falls back to 10 Mbps.
|
|
110
131
|
val bitrateStr = if (videoBitrate > 0) "$videoBitrate" else "10M"
|
|
@@ -118,21 +139,22 @@ object VideoTrimmerUtil {
|
|
|
118
139
|
// h264_mediacodec: Android's hardware H.264 encoder — fast and energy-efficient.
|
|
119
140
|
// Note: Android FFmpegKit auto-rotates by default, so no -noautorotate is needed.
|
|
120
141
|
// The transpose filters above only handle user-initiated rotation, not source metadata.
|
|
121
|
-
cmds.addAll(listOf(
|
|
122
|
-
|
|
123
|
-
"-
|
|
124
|
-
"-
|
|
125
|
-
"-
|
|
126
|
-
|
|
127
|
-
))
|
|
142
|
+
cmds.addAll(listOf("-c:v", "h264_mediacodec", "-b:v", bitrateStr))
|
|
143
|
+
when {
|
|
144
|
+
removeAudio -> cmds.add("-an")
|
|
145
|
+
speed != 1.0 -> cmds.addAll(listOf("-af", buildAtempoChain(speed)))
|
|
146
|
+
else -> cmds.addAll(listOf("-c:a", "copy"))
|
|
147
|
+
}
|
|
148
|
+
cmds.addAll(listOf("-metadata", "creation_time=$formattedDateTime", outputFile))
|
|
128
149
|
} else {
|
|
129
150
|
// Stream copy: no re-encoding, extremely fast but only cuts at keyframes.
|
|
130
|
-
cmds.addAll(listOf(
|
|
131
|
-
|
|
132
|
-
"-c", "copy",
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
cmds.addAll(listOf("-i", inputFile))
|
|
152
|
+
if (removeAudio) {
|
|
153
|
+
cmds.addAll(listOf("-c:v", "copy", "-an"))
|
|
154
|
+
} else {
|
|
155
|
+
cmds.addAll(listOf("-c", "copy"))
|
|
156
|
+
}
|
|
157
|
+
cmds.addAll(listOf("-metadata", "creation_time=$formattedDateTime", outputFile))
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
val command = cmds.toTypedArray()
|
|
@@ -13,6 +13,7 @@ import android.media.MediaExtractor
|
|
|
13
13
|
import android.media.MediaFormat
|
|
14
14
|
import android.media.MediaMetadataRetriever
|
|
15
15
|
import android.media.MediaPlayer
|
|
16
|
+
import android.media.PlaybackParams
|
|
16
17
|
import android.net.Uri
|
|
17
18
|
import android.os.Build
|
|
18
19
|
import android.os.Handler
|
|
@@ -202,9 +203,17 @@ class VideoTrimmerView(
|
|
|
202
203
|
private lateinit var cropBtn: ImageView
|
|
203
204
|
private lateinit var undoBtn: ImageView
|
|
204
205
|
private lateinit var redoBtn: ImageView
|
|
206
|
+
private lateinit var muteBtn: ImageView
|
|
207
|
+
private lateinit var speedBtn: TextView
|
|
205
208
|
private lateinit var videoContainer: FrameLayout
|
|
206
209
|
private var cropOverlay: CropOverlayView? = null
|
|
207
210
|
|
|
211
|
+
internal var isMuted = false
|
|
212
|
+
private set
|
|
213
|
+
private var configRemoveAudio = false
|
|
214
|
+
private var speed: Double = 1.0
|
|
215
|
+
private val speedOptions = doubleArrayOf(0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0)
|
|
216
|
+
|
|
208
217
|
private lateinit var trimmerView: RelativeLayout
|
|
209
218
|
|
|
210
219
|
private var trimmerColor = context.getString(R.string.trim_color).toColorInt()
|
|
@@ -283,6 +292,8 @@ class VideoTrimmerView(
|
|
|
283
292
|
flipBtn = findViewById(R.id.flipBtn)
|
|
284
293
|
rotateBtn = findViewById(R.id.rotateBtn)
|
|
285
294
|
cropBtn = findViewById(R.id.cropBtn)
|
|
295
|
+
muteBtn = findViewById(R.id.muteBtn)
|
|
296
|
+
speedBtn = findViewById(R.id.speedBtn)
|
|
286
297
|
undoBtn = findViewById(R.id.undoBtn)
|
|
287
298
|
redoBtn = findViewById(R.id.redoBtn)
|
|
288
299
|
videoContainer = findViewById(R.id.videoContainer)
|
|
@@ -510,6 +521,9 @@ class VideoTrimmerView(
|
|
|
510
521
|
playOrPause()
|
|
511
522
|
}
|
|
512
523
|
|
|
524
|
+
mediaPlayer?.setVolume(if (isMuted) 0f else 1f, if (isMuted) 0f else 1f)
|
|
525
|
+
applyPlaybackSpeed()
|
|
526
|
+
|
|
513
527
|
if (isVideoType) {
|
|
514
528
|
transformRow.alpha = 0f
|
|
515
529
|
transformRow.visibility = View.VISIBLE
|
|
@@ -568,11 +582,56 @@ class VideoTrimmerView(
|
|
|
568
582
|
seekTo(startTime, true)
|
|
569
583
|
}
|
|
570
584
|
player.start()
|
|
585
|
+
applyPlaybackSpeed()
|
|
571
586
|
startTimingRunnable()
|
|
572
587
|
}
|
|
573
588
|
setPlayPauseViewIcon(player.isPlaying)
|
|
574
589
|
}
|
|
575
590
|
|
|
591
|
+
private fun applyPlaybackSpeed() {
|
|
592
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
593
|
+
val player = mediaPlayer ?: return
|
|
594
|
+
val params = player.playbackParams ?: PlaybackParams()
|
|
595
|
+
player.playbackParams = params.setSpeed(speed.toFloat())
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private fun onMuteTapped() {
|
|
600
|
+
isMuted = !isMuted
|
|
601
|
+
muteBtn.setImageResource(if (isMuted) R.drawable.speaker_slash_fill else R.drawable.speaker_wave_2_fill)
|
|
602
|
+
mediaPlayer?.setVolume(if (isMuted) 0f else 1f, if (isMuted) 0f else 1f)
|
|
603
|
+
if (enableHapticFeedback) {
|
|
604
|
+
performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Uses Android's native PopupMenu anchored to the speed button for a platform-
|
|
609
|
+
// consistent speed selector (equivalent to iOS's UIMenu on iOS 14+).
|
|
610
|
+
private fun onSpeedTapped() {
|
|
611
|
+
val popup = android.widget.PopupMenu(context, speedBtn)
|
|
612
|
+
speedOptions.forEachIndexed { index, opt ->
|
|
613
|
+
val title = if (opt == 1.0) "Normal (1x)" else "${opt}x"
|
|
614
|
+
popup.menu.add(0, index, index, title)
|
|
615
|
+
}
|
|
616
|
+
popup.setOnMenuItemClickListener { item ->
|
|
617
|
+
setSpeed(speedOptions[item.itemId])
|
|
618
|
+
true
|
|
619
|
+
}
|
|
620
|
+
popup.show()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private fun setSpeed(newSpeed: Double) {
|
|
624
|
+
speed = newSpeed
|
|
625
|
+
speedBtn.text = if (newSpeed == 1.0) "1x" else "${newSpeed}x"
|
|
626
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
627
|
+
mediaPlayer?.playbackParams = mediaPlayer?.playbackParams?.setSpeed(newSpeed.toFloat())
|
|
628
|
+
?: PlaybackParams().setSpeed(newSpeed.toFloat())
|
|
629
|
+
}
|
|
630
|
+
if (enableHapticFeedback) {
|
|
631
|
+
performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
576
635
|
fun onMediaPause() {
|
|
577
636
|
mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
|
|
578
637
|
val player = mediaPlayer ?: return
|
|
@@ -598,6 +657,8 @@ class VideoTrimmerView(
|
|
|
598
657
|
cropBtn.setOnClickListener { onCropTapped() }
|
|
599
658
|
undoBtn.setOnClickListener { onUndoTapped() }
|
|
600
659
|
redoBtn.setOnClickListener { onRedoTapped() }
|
|
660
|
+
muteBtn.setOnClickListener { onMuteTapped() }
|
|
661
|
+
speedBtn.setOnClickListener { onSpeedTapped() }
|
|
601
662
|
|
|
602
663
|
cropBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
603
664
|
undoBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
@@ -614,6 +675,7 @@ class VideoTrimmerView(
|
|
|
614
675
|
?.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
|
615
676
|
?.toLongOrNull() ?: 0L
|
|
616
677
|
}
|
|
678
|
+
val effectiveRemoveAudio = isMuted || configRemoveAudio
|
|
617
679
|
ffmpegSession = VideoTrimmerUtil.trim(
|
|
618
680
|
mSourceUri.toString(),
|
|
619
681
|
StorageUtil.getOutputPath(mContext, mOutputExt),
|
|
@@ -627,6 +689,8 @@ class VideoTrimmerView(
|
|
|
627
689
|
vh,
|
|
628
690
|
bitrate,
|
|
629
691
|
enablePreciseTrimming,
|
|
692
|
+
effectiveRemoveAudio,
|
|
693
|
+
speed,
|
|
630
694
|
mOnTrimVideoListener
|
|
631
695
|
)
|
|
632
696
|
}
|
|
@@ -755,6 +819,17 @@ class VideoTrimmerView(
|
|
|
755
819
|
enablePreciseTrimming = config.hasKey("enablePreciseTrimming") && config.getBoolean("enablePreciseTrimming")
|
|
756
820
|
autoplay = config.hasKey("autoplay") && config.getBoolean("autoplay")
|
|
757
821
|
|
|
822
|
+
if (config.hasKey("removeAudio")) {
|
|
823
|
+
configRemoveAudio = config.getBoolean("removeAudio")
|
|
824
|
+
isMuted = configRemoveAudio
|
|
825
|
+
}
|
|
826
|
+
if (config.hasKey("speed") && config.getDouble("speed") > 0) {
|
|
827
|
+
speed = config.getDouble("speed")
|
|
828
|
+
speedBtn.text = if (speed == 1.0) "1x" else "${speed}x"
|
|
829
|
+
}
|
|
830
|
+
muteBtn.setImageResource(if (isMuted) R.drawable.speaker_slash_fill else R.drawable.speaker_wave_2_fill)
|
|
831
|
+
muteBtn.visibility = if (isVideoType) View.VISIBLE else View.GONE
|
|
832
|
+
|
|
758
833
|
if (config.hasKey("jumpToPositionOnLoad") && config.getDouble("jumpToPositionOnLoad") > 0) {
|
|
759
834
|
jumpToPositionOnLoad = maxOf(0, (config.getDouble("jumpToPositionOnLoad") * 1000L).toLong())
|
|
760
835
|
}
|
|
@@ -856,6 +931,8 @@ class VideoTrimmerView(
|
|
|
856
931
|
cropBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
857
932
|
undoBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
858
933
|
redoBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
934
|
+
muteBtn.setColorFilter(iconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
935
|
+
speedBtn.setTextColor(iconColor)
|
|
859
936
|
|
|
860
937
|
// Overlays
|
|
861
938
|
leadingOverlay.setBackgroundColor(overlayColor)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="20.574dp"
|
|
3
|
+
android:height="21.997dp"
|
|
4
|
+
android:viewportWidth="20.574"
|
|
5
|
+
android:viewportHeight="21.997">
|
|
6
|
+
<path
|
|
7
|
+
android:fillColor="#FF000000"
|
|
8
|
+
android:pathData="M0,0h20.574v21.997h-20.574z"
|
|
9
|
+
android:strokeAlpha="0"
|
|
10
|
+
android:fillAlpha="0"/>
|
|
11
|
+
<path
|
|
12
|
+
android:pathData="M14.746,18.846C14.618,19.308 14.214,19.617 13.685,19.617C13.236,19.617 12.855,19.431 12.406,19.001L8.246,15.056C8.177,14.998 8.099,14.968 8.002,14.968L5.199,14.968C3.881,14.968 3.158,14.246 3.158,12.83L3.158,9.197C3.158,8.561 3.306,8.063 3.59,7.71ZM14.789,3.543L14.789,13.536L8.231,6.982C8.236,6.978 8.241,6.974 8.246,6.97L12.406,3.064C12.904,2.585 13.217,2.39 13.666,2.39C14.33,2.39 14.789,2.908 14.789,3.543Z"
|
|
13
|
+
android:fillColor="#ffffff"
|
|
14
|
+
android:fillAlpha="0.85"/>
|
|
15
|
+
<path
|
|
16
|
+
android:pathData="M18.158,20.642C18.451,20.935 18.929,20.935 19.222,20.642C19.506,20.339 19.515,19.871 19.222,19.578L2.494,2.859C2.201,2.566 1.713,2.566 1.42,2.859C1.136,3.142 1.136,3.64 1.42,3.923Z"
|
|
17
|
+
android:fillColor="#ffffff"
|
|
18
|
+
android:fillAlpha="0.85"/>
|
|
19
|
+
</vector>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="22.158dp"
|
|
3
|
+
android:height="17.236dp"
|
|
4
|
+
android:viewportWidth="22.158"
|
|
5
|
+
android:viewportHeight="17.236">
|
|
6
|
+
<path
|
|
7
|
+
android:fillColor="#FF000000"
|
|
8
|
+
android:pathData="M0,0h22.158v17.236h-22.158z"
|
|
9
|
+
android:strokeAlpha="0"
|
|
10
|
+
android:fillAlpha="0"/>
|
|
11
|
+
<path
|
|
12
|
+
android:pathData="M18.74,15.322C19.102,15.566 19.551,15.479 19.815,15.107C21.065,13.389 21.797,11.025 21.797,8.613C21.797,6.201 21.074,3.818 19.815,2.119C19.551,1.748 19.102,1.66 18.74,1.904C18.379,2.148 18.32,2.607 18.604,3.008C19.668,4.512 20.283,6.533 20.283,8.613C20.283,10.693 19.648,12.695 18.604,14.219C18.33,14.619 18.379,15.078 18.74,15.322Z"
|
|
13
|
+
android:fillColor="#ffffff"
|
|
14
|
+
android:fillAlpha="0.85"/>
|
|
15
|
+
<path
|
|
16
|
+
android:pathData="M15.127,12.773C15.449,12.998 15.908,12.93 16.172,12.549C16.924,11.563 17.373,10.107 17.373,8.613C17.373,7.119 16.924,5.674 16.172,4.678C15.908,4.297 15.449,4.219 15.127,4.453C14.727,4.727 14.668,5.215 14.971,5.615C15.537,6.396 15.859,7.48 15.859,8.613C15.859,9.746 15.527,10.82 14.971,11.611C14.678,12.021 14.727,12.49 15.127,12.773Z"
|
|
17
|
+
android:fillColor="#ffffff"
|
|
18
|
+
android:fillAlpha="0.85"/>
|
|
19
|
+
<path
|
|
20
|
+
android:pathData="M10.527,17.236C11.172,17.236 11.631,16.777 11.631,16.143L11.631,1.162C11.631,0.527 11.172,0.01 10.508,0.01C10.049,0.01 9.746,0.215 9.248,0.684L5.078,4.59C5.02,4.648 4.932,4.678 4.834,4.678L2.041,4.678C0.713,4.678 0,5.41 0,6.816L0,10.449C0,11.865 0.713,12.588 2.041,12.588L4.834,12.588C4.932,12.588 5.02,12.617 5.078,12.676L9.248,16.621C9.688,17.051 10.068,17.236 10.527,17.236Z"
|
|
21
|
+
android:fillColor="#ffffff"
|
|
22
|
+
android:fillAlpha="0.85"/>
|
|
23
|
+
</vector>
|
|
@@ -79,6 +79,28 @@
|
|
|
79
79
|
android:tint="#80FFFFFF"
|
|
80
80
|
android:scaleType="fitCenter"
|
|
81
81
|
android:contentDescription="Crop" />
|
|
82
|
+
|
|
83
|
+
<ImageView
|
|
84
|
+
android:id="@+id/muteBtn"
|
|
85
|
+
android:layout_width="22dp"
|
|
86
|
+
android:layout_height="22dp"
|
|
87
|
+
android:src="@drawable/speaker_wave_2_fill"
|
|
88
|
+
android:tint="@android:color/white"
|
|
89
|
+
android:scaleType="fitCenter"
|
|
90
|
+
android:layout_marginStart="12dp"
|
|
91
|
+
android:contentDescription="Mute" />
|
|
92
|
+
|
|
93
|
+
<TextView
|
|
94
|
+
android:id="@+id/speedBtn"
|
|
95
|
+
android:layout_width="wrap_content"
|
|
96
|
+
android:layout_height="22dp"
|
|
97
|
+
android:text="1x"
|
|
98
|
+
android:textColor="@android:color/white"
|
|
99
|
+
android:textSize="13sp"
|
|
100
|
+
android:textStyle="bold"
|
|
101
|
+
android:gravity="center"
|
|
102
|
+
android:layout_marginStart="12dp"
|
|
103
|
+
android:contentDescription="Speed" />
|
|
82
104
|
</LinearLayout>
|
|
83
105
|
|
|
84
106
|
<View
|
|
@@ -3,6 +3,7 @@ package com.videotrim
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import com.facebook.react.bridge.Promise
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
7
|
import com.facebook.react.bridge.ReadableMap
|
|
7
8
|
import com.facebook.react.bridge.WritableMap
|
|
8
9
|
|
|
@@ -69,6 +70,38 @@ class VideoTrimModule(
|
|
|
69
70
|
base.trim(url, options, promise)
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
override fun getFrameAt(url: String, options: ReadableMap, promise: Promise) {
|
|
74
|
+
base.getFrameAt(url, options, promise)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun extractAudio(url: String, options: ReadableMap, promise: Promise) {
|
|
78
|
+
base.extractAudio(url, options, promise)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun compress(url: String, options: ReadableMap, promise: Promise) {
|
|
82
|
+
base.compress(url, options, promise)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override fun toGif(url: String, options: ReadableMap, promise: Promise) {
|
|
86
|
+
base.toGif(url, options, promise)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun merge(urls: ReadableArray, options: ReadableMap, promise: Promise) {
|
|
90
|
+
base.merge(urls, options, promise)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun saveToPhoto(filePath: String, promise: Promise) {
|
|
94
|
+
base.saveToPhoto(filePath, promise)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun saveToDocuments(filePath: String, promise: Promise) {
|
|
98
|
+
base.saveToDocuments(filePath, promise)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun share(filePath: String, promise: Promise) {
|
|
102
|
+
base.share(filePath, promise)
|
|
103
|
+
}
|
|
104
|
+
|
|
72
105
|
companion object {
|
|
73
106
|
const val NAME = "VideoTrim"
|
|
74
107
|
}
|
|
@@ -3,6 +3,7 @@ package com.videotrim
|
|
|
3
3
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
4
|
import com.facebook.react.bridge.Arguments
|
|
5
5
|
import com.facebook.react.bridge.WritableMap
|
|
6
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
7
|
|
|
7
8
|
import com.facebook.react.bridge.*
|
|
8
9
|
import com.facebook.react.module.annotations.ReactModule
|
|
@@ -69,6 +70,46 @@ class VideoTrimModule internal constructor(context: ReactApplicationContext) : V
|
|
|
69
70
|
base.trim(url, options, promise)
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
@ReactMethod
|
|
74
|
+
override fun getFrameAt(url: String, options: ReadableMap?, promise: Promise) {
|
|
75
|
+
base.getFrameAt(url, options, promise)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@ReactMethod
|
|
79
|
+
override fun extractAudio(url: String, options: ReadableMap?, promise: Promise) {
|
|
80
|
+
base.extractAudio(url, options, promise)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@ReactMethod
|
|
84
|
+
override fun compress(url: String, options: ReadableMap?, promise: Promise) {
|
|
85
|
+
base.compress(url, options, promise)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@ReactMethod
|
|
89
|
+
override fun toGif(url: String, options: ReadableMap?, promise: Promise) {
|
|
90
|
+
base.toGif(url, options, promise)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@ReactMethod
|
|
94
|
+
override fun merge(urls: ReadableArray, options: ReadableMap?, promise: Promise) {
|
|
95
|
+
base.merge(urls, options, promise)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@ReactMethod
|
|
99
|
+
override fun saveToPhoto(filePath: String, promise: Promise) {
|
|
100
|
+
base.saveToPhoto(filePath, promise)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@ReactMethod
|
|
104
|
+
override fun saveToDocuments(filePath: String, promise: Promise) {
|
|
105
|
+
base.saveToDocuments(filePath, promise)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@ReactMethod
|
|
109
|
+
override fun share(filePath: String, promise: Promise) {
|
|
110
|
+
base.share(filePath, promise)
|
|
111
|
+
}
|
|
112
|
+
|
|
72
113
|
companion object {
|
|
73
114
|
const val NAME = "VideoTrim"
|
|
74
115
|
}
|
|
@@ -3,6 +3,7 @@ package com.videotrim
|
|
|
3
3
|
import com.facebook.react.bridge.Promise
|
|
4
4
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
5
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
7
|
import com.facebook.react.bridge.ReadableMap
|
|
7
8
|
|
|
8
9
|
abstract class VideoTrimSpec internal constructor(context: ReactApplicationContext) :
|
|
@@ -21,4 +22,20 @@ abstract class VideoTrimSpec internal constructor(context: ReactApplicationConte
|
|
|
21
22
|
abstract fun isValidFile(url: String, promise: Promise)
|
|
22
23
|
|
|
23
24
|
abstract fun trim(url: String, options: ReadableMap?, promise: Promise)
|
|
25
|
+
|
|
26
|
+
abstract fun getFrameAt(url: String, options: ReadableMap?, promise: Promise)
|
|
27
|
+
|
|
28
|
+
abstract fun extractAudio(url: String, options: ReadableMap?, promise: Promise)
|
|
29
|
+
|
|
30
|
+
abstract fun compress(url: String, options: ReadableMap?, promise: Promise)
|
|
31
|
+
|
|
32
|
+
abstract fun toGif(url: String, options: ReadableMap?, promise: Promise)
|
|
33
|
+
|
|
34
|
+
abstract fun merge(urls: ReadableArray, options: ReadableMap?, promise: Promise)
|
|
35
|
+
|
|
36
|
+
abstract fun saveToPhoto(filePath: String, promise: Promise)
|
|
37
|
+
|
|
38
|
+
abstract fun saveToDocuments(filePath: String, promise: Promise)
|
|
39
|
+
|
|
40
|
+
abstract fun share(filePath: String, promise: Promise)
|
|
24
41
|
}
|