react-native-video-trim 7.1.1 → 8.1.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,14 +203,23 @@ 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()
211
220
  private var handleIconColor = Color.BLACK
212
221
  private var isLightTheme = false
222
+ private var durationFormat: String = "mm:ss.SSS"
213
223
  private val iconColor: Int get() = if (isLightTheme) Color.BLACK else Color.WHITE
214
224
  private val dimmedIconColor: Int get() = if (isLightTheme) Color.argb(128, 0, 0, 0) else Color.argb(128, 255, 255, 255)
215
225
  private lateinit var leadingChevron: ImageView
@@ -283,6 +293,8 @@ class VideoTrimmerView(
283
293
  flipBtn = findViewById(R.id.flipBtn)
284
294
  rotateBtn = findViewById(R.id.rotateBtn)
285
295
  cropBtn = findViewById(R.id.cropBtn)
296
+ muteBtn = findViewById(R.id.muteBtn)
297
+ speedBtn = findViewById(R.id.speedBtn)
286
298
  undoBtn = findViewById(R.id.undoBtn)
287
299
  redoBtn = findViewById(R.id.redoBtn)
288
300
  videoContainer = findViewById(R.id.videoContainer)
@@ -510,6 +522,9 @@ class VideoTrimmerView(
510
522
  playOrPause()
511
523
  }
512
524
 
525
+ mediaPlayer?.setVolume(if (isMuted) 0f else 1f, if (isMuted) 0f else 1f)
526
+ applyPlaybackSpeed()
527
+
513
528
  if (isVideoType) {
514
529
  transformRow.alpha = 0f
515
530
  transformRow.visibility = View.VISIBLE
@@ -568,11 +583,56 @@ class VideoTrimmerView(
568
583
  seekTo(startTime, true)
569
584
  }
570
585
  player.start()
586
+ applyPlaybackSpeed()
571
587
  startTimingRunnable()
572
588
  }
573
589
  setPlayPauseViewIcon(player.isPlaying)
574
590
  }
575
591
 
592
+ private fun applyPlaybackSpeed() {
593
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
594
+ val player = mediaPlayer ?: return
595
+ val params = player.playbackParams ?: PlaybackParams()
596
+ player.playbackParams = params.setSpeed(speed.toFloat())
597
+ }
598
+ }
599
+
600
+ private fun onMuteTapped() {
601
+ isMuted = !isMuted
602
+ muteBtn.setImageResource(if (isMuted) R.drawable.speaker_slash_fill else R.drawable.speaker_wave_2_fill)
603
+ mediaPlayer?.setVolume(if (isMuted) 0f else 1f, if (isMuted) 0f else 1f)
604
+ if (enableHapticFeedback) {
605
+ performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
606
+ }
607
+ }
608
+
609
+ // Uses Android's native PopupMenu anchored to the speed button for a platform-
610
+ // consistent speed selector (equivalent to iOS's UIMenu on iOS 14+).
611
+ private fun onSpeedTapped() {
612
+ val popup = android.widget.PopupMenu(context, speedBtn)
613
+ speedOptions.forEachIndexed { index, opt ->
614
+ val title = if (opt == 1.0) "Normal (1x)" else "${opt}x"
615
+ popup.menu.add(0, index, index, title)
616
+ }
617
+ popup.setOnMenuItemClickListener { item ->
618
+ setSpeed(speedOptions[item.itemId])
619
+ true
620
+ }
621
+ popup.show()
622
+ }
623
+
624
+ private fun setSpeed(newSpeed: Double) {
625
+ speed = newSpeed
626
+ speedBtn.text = if (newSpeed == 1.0) "1x" else "${newSpeed}x"
627
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
628
+ mediaPlayer?.playbackParams = mediaPlayer?.playbackParams?.setSpeed(newSpeed.toFloat())
629
+ ?: PlaybackParams().setSpeed(newSpeed.toFloat())
630
+ }
631
+ if (enableHapticFeedback) {
632
+ performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY)
633
+ }
634
+ }
635
+
576
636
  fun onMediaPause() {
577
637
  mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
578
638
  val player = mediaPlayer ?: return
@@ -598,6 +658,8 @@ class VideoTrimmerView(
598
658
  cropBtn.setOnClickListener { onCropTapped() }
599
659
  undoBtn.setOnClickListener { onUndoTapped() }
600
660
  redoBtn.setOnClickListener { onRedoTapped() }
661
+ muteBtn.setOnClickListener { onMuteTapped() }
662
+ speedBtn.setOnClickListener { onSpeedTapped() }
601
663
 
602
664
  cropBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
603
665
  undoBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
@@ -614,6 +676,7 @@ class VideoTrimmerView(
614
676
  ?.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
615
677
  ?.toLongOrNull() ?: 0L
616
678
  }
679
+ val effectiveRemoveAudio = isMuted || configRemoveAudio
617
680
  ffmpegSession = VideoTrimmerUtil.trim(
618
681
  mSourceUri.toString(),
619
682
  StorageUtil.getOutputPath(mContext, mOutputExt),
@@ -627,6 +690,8 @@ class VideoTrimmerView(
627
690
  vh,
628
691
  bitrate,
629
692
  enablePreciseTrimming,
693
+ effectiveRemoveAudio,
694
+ speed,
630
695
  mOnTrimVideoListener
631
696
  )
632
697
  }
@@ -741,6 +806,7 @@ class VideoTrimmerView(
741
806
  }
742
807
 
743
808
  isLightTheme = config.hasKey("theme") && config.getString("theme") == "light"
809
+ durationFormat = if (config.hasKey("durationFormat")) config.getString("durationFormat") ?: "mm:ss.SSS" else "mm:ss.SSS"
744
810
 
745
811
  cancelBtn.text = config.getString("cancelButtonText")
746
812
  saveBtn.text = config.getString("saveButtonText")
@@ -755,6 +821,17 @@ class VideoTrimmerView(
755
821
  enablePreciseTrimming = config.hasKey("enablePreciseTrimming") && config.getBoolean("enablePreciseTrimming")
756
822
  autoplay = config.hasKey("autoplay") && config.getBoolean("autoplay")
757
823
 
824
+ if (config.hasKey("removeAudio")) {
825
+ configRemoveAudio = config.getBoolean("removeAudio")
826
+ isMuted = configRemoveAudio
827
+ }
828
+ if (config.hasKey("speed") && config.getDouble("speed") > 0) {
829
+ speed = config.getDouble("speed")
830
+ speedBtn.text = if (speed == 1.0) "1x" else "${speed}x"
831
+ }
832
+ muteBtn.setImageResource(if (isMuted) R.drawable.speaker_slash_fill else R.drawable.speaker_wave_2_fill)
833
+ muteBtn.visibility = if (isVideoType) View.VISIBLE else View.GONE
834
+
758
835
  if (config.hasKey("jumpToPositionOnLoad") && config.getDouble("jumpToPositionOnLoad") > 0) {
759
836
  jumpToPositionOnLoad = maxOf(0, (config.getDouble("jumpToPositionOnLoad") * 1000L).toLong())
760
837
  }
@@ -856,6 +933,8 @@ class VideoTrimmerView(
856
933
  cropBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
857
934
  undoBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
858
935
  redoBtn.setColorFilter(dimmedIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
936
+ muteBtn.setColorFilter(iconColor, android.graphics.PorterDuff.Mode.SRC_IN)
937
+ speedBtn.setTextColor(iconColor)
859
938
 
860
939
  // Overlays
861
940
  leadingOverlay.setBackgroundColor(overlayColor)
@@ -936,11 +1015,19 @@ class VideoTrimmerView(
936
1015
  }
937
1016
 
938
1017
  private fun formatTime(milliseconds: Int): String {
939
- val totalSeconds = milliseconds / 1000
940
- val minutes = totalSeconds / 60
941
- val seconds = totalSeconds % 60
942
- val millis = milliseconds % 1000
943
- return String.format(Locale.getDefault(), "%d:%02d.%03d", minutes, seconds, millis)
1018
+ val totalMs = if (milliseconds < 0) 0 else milliseconds
1019
+ val h = totalMs / 3_600_000
1020
+ val m = (totalMs / 60_000) % 60
1021
+ val s = (totalMs / 1000) % 60
1022
+ val ms = totalMs % 1000
1023
+ val locale = Locale.getDefault()
1024
+ return when (durationFormat) {
1025
+ "mm:ss" -> String.format(locale, "%02d:%02d", m + h * 60, s)
1026
+ "mm:ss.SS" -> String.format(locale, "%02d:%02d.%02d", m + h * 60, s, ms / 10)
1027
+ "hh:mm:ss" -> String.format(locale, "%02d:%02d:%02d", h, m, s)
1028
+ "hh:mm:ss.SSS" -> String.format(locale, "%02d:%02d:%02d.%03d", h, m, s, ms)
1029
+ else -> String.format(locale, "%02d:%02d.%03d", m + h * 60, s, ms)
1030
+ }
944
1031
  }
945
1032
 
946
1033
  @Suppress("ClickableViewAccessibility")
@@ -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
@@ -271,7 +293,7 @@
271
293
  android:focusable="true"
272
294
  android:gravity="center"
273
295
  android:padding="10dp"
274
- android:text="00:00.00"
296
+ android:text="00:00"
275
297
  android:textColor="@android:color/white"
276
298
  android:textSize="16sp" />
277
299
 
@@ -284,7 +306,7 @@
284
306
  android:focusable="true"
285
307
  android:gravity="center"
286
308
  android:padding="10dp"
287
- android:text="00:00.00"
309
+ android:text="00:00"
288
310
  android:textColor="@android:color/white"
289
311
  android:textSize="16sp" />
290
312
 
@@ -297,7 +319,7 @@
297
319
  android:focusable="true"
298
320
  android:gravity="center"
299
321
  android:padding="10dp"
300
- android:text="00:00.00"
322
+ android:text="00:00"
301
323
  android:textColor="@android:color/white"
302
324
  android:textSize="16sp" />
303
325
  </FrameLayout>
@@ -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
  }