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.
@@ -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 filesDir = context.filesDir
35
- val files = filesDir.listFiles { _, name -> name.startsWith(VideoTrimmerUtil.FILE_PREFIX) }
36
-
37
- return files?.map { it.absolutePath }?.toTypedArray() ?: emptyArray()
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 allowedDir = BaseUtils.getContext().filesDir.canonicalFile
44
- if (!file.path.startsWith(allowedDir.path)) return false
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
- @Throws(IOException::class)
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
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
70
- saveVideoUsingMediaStore(context, videoFile)
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
- try {
73
- saveVideoUsingTraditionalStorage(context, videoFile)
74
- } catch (e: IOException) {
75
- throw RuntimeException(e)
76
- }
96
+ saveVideoToGallery(context, file)
77
97
  }
78
98
  }
79
99
 
80
- private fun saveVideoUsingMediaStore(context: Context, videoFile: File) {
81
- val values = ContentValues().apply {
82
- put(MediaStore.Video.Media.TITLE, "My Video Title")
83
- put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
84
- put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
85
- }
86
- val uri: Uri? = context.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values)
87
-
88
- if (uri != null) {
89
- try {
90
- val outputStream = context.contentResolver.openOutputStream(uri)
91
- copyFile(videoFile, outputStream!!)
92
- MediaScannerConnection.scanFile(context, arrayOf(uri.toString()), arrayOf("video/*"), null)
93
- } catch (e: IOException) {
94
- e.printStackTrace()
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 saveVideoUsingTraditionalStorage(context: Context, videoFile: File) {
101
- val galleryDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
102
- val destinationFile = File(galleryDirectory, videoFile.name)
103
- copyFile(videoFile, destinationFile)
104
- MediaScannerConnection.scanFile(context, arrayOf(destinationFile.absolutePath), arrayOf("video/*"), null)
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 all three cases, -c copy won't work because
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
- "-c:v", "h264_mediacodec",
123
- "-b:v", bitrateStr,
124
- "-c:a", "copy",
125
- "-metadata", "creation_time=$formattedDateTime",
126
- outputFile
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
- "-i", inputFile,
132
- "-c", "copy",
133
- "-metadata", "creation_time=$formattedDateTime",
134
- outputFile
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
  }