react-native-video-trim 7.0.1 → 7.1.1

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 CHANGED
@@ -12,6 +12,7 @@
12
12
  * [Behavior Options](#behavior-options)
13
13
  - [Platform Setup](#platform-setup)
14
14
  - [Advanced Features](#advanced-features)
15
+ * [Theming](#theming)
15
16
  * [Audio Trimming](#audio-trimming)
16
17
  * [Remote Files (HTTPS)](#remote-files-https)
17
18
  * [Video Transforms (Flip, Rotate, Crop)](#video-transforms-flip-rotate-crop)
@@ -24,8 +25,7 @@
24
25
  <div align="center">
25
26
  <h2>📱 Professional video trimmer for React Native apps</h2>
26
27
 
27
- <img src="images/android.gif" width="300" />
28
- <img src="images/ios.gif" width="300" />
28
+ <img src="images/ios.png" width="300" />
29
29
 
30
30
  <p>
31
31
  <strong>✅ iOS & Android</strong> •
@@ -40,7 +40,7 @@ A powerful, easy-to-use video and audio trimming library for React Native applic
40
40
 
41
41
  ### ✨ Key Features
42
42
 
43
- - **📹 Video & Audio Support** - Trim both video and audio files
43
+ - **📹 Video & Audio Support** - Trim both video and audio files with waveform visualization
44
44
  - **🔄 Flip, Rotate & Crop** - Built-in video transforms with undo/redo support
45
45
  - **🎯 Precise Trimming** - Optional frame-accurate cuts via hardware-accelerated re-encoding
46
46
  - **🌐 Local & Remote Files** - Support for local storage and HTTPS URLs
@@ -48,18 +48,20 @@ A powerful, easy-to-use video and audio trimming library for React Native applic
48
48
  - **✅ File Validation** - Built-in validation for media files
49
49
  - **🗂️ File Management** - List, clean up, and delete specific files
50
50
  - **🔄 Universal Architecture** - Works with both New and Old React Native architectures
51
+ - **🎨 Dark & Light Theme** - Built-in dark and light theme support
51
52
 
52
53
  ### 🎛️ Core Capabilities
53
54
 
54
55
  | Feature | Description |
55
56
  |---------|-------------|
56
- | **Trimming** | Video/audio trimming with visual timeline controls |
57
+ | **Trimming** | Video/audio trimming with visual timeline controls and audio waveform |
57
58
  | **Transforms** | Horizontal flip, 90° rotation, and freeform crop with undo/redo |
58
59
  | **Precise Trimming** | Frame-accurate cuts using hardware re-encoding (opt-in) |
59
60
  | **Validation** | Check if files are valid video/audio before processing |
60
61
  | **Save Options** | Photos, Documents, Share sheet integration |
61
62
  | **File Management** | Complete file lifecycle management |
62
63
  | **Customization** | Extensive UI and behavior customization |
64
+ | **Theming** | Dark and light theme with automatic color adaptation |
63
65
 
64
66
  <div align="center">
65
67
  <img src="images/document_picker.png" width="250" />
@@ -282,16 +284,29 @@ All configuration options are optional. Here are the most commonly used ones:
282
284
 
283
285
  | Option | Type | Default | Description |
284
286
  |--------|------|---------|-------------|
287
+ | `theme` | `'dark' \| 'light'` | `'dark'` | Editor color theme (see [Theming](#theming)) |
285
288
  | `cancelButtonText` | `string` | `"Cancel"` | Cancel button text |
286
289
  | `saveButtonText` | `string` | `"Save"` | Save button text |
287
290
  | `trimmingText` | `string` | `"Trimming video..."` | Progress dialog text |
288
291
  | `headerText` | `string` | - | Header text |
289
292
  | `headerTextSize` | `number` | `16` | Header text size |
290
- | `headerTextColor` | `string` | - | Header text color |
293
+ | `headerTextColor` | `string` | - | Header text color (defaults to black in light theme, white in dark theme) |
291
294
  | `trimmerColor` | `string` | - | Trimmer bar color |
292
- | `handleIconColor` | `string` | - | Trimmer left/right handles color |
295
+ | `handleIconColor` | `string` | - | Trimmer left/right handles color (defaults to black in light theme, white in dark theme) |
293
296
  | `fullScreenModalIOS` | `boolean` | `false` | Use fullscreen modal on iOS |
294
297
 
298
+ ### Audio Waveform Options
299
+
300
+ These options only apply when `type: 'audio'`. The waveform replaces the thumbnail track with a bar visualization of the audio amplitude.
301
+
302
+ | Option | Type | Default | Description |
303
+ |--------|------|---------|-------------|
304
+ | `waveformColor` | `string` | `"white"` | Fill color of the waveform bars |
305
+ | `waveformBackgroundColor` | `string` | `"#3478F6"` | Background color behind the bars |
306
+ | `waveformBarWidth` | `number` | `3` | Width of each bar in dp/pt |
307
+ | `waveformBarGap` | `number` | `2` | Gap between bars in dp/pt |
308
+ | `waveformBarCornerRadius` | `number` | `1.5` | Corner radius of each bar in dp/pt |
309
+
295
310
  ### Dialog Options
296
311
 
297
312
  <details>
@@ -367,6 +382,7 @@ showEditor(videoPath, {
367
382
  removeAfterSavedToPhoto: true,
368
383
 
369
384
  // UI customization
385
+ theme: 'light',
370
386
  headerText: "Trim Your Video",
371
387
  cancelButtonText: "Back",
372
388
  saveButtonText: "Done",
@@ -398,14 +414,42 @@ buildscript {
398
414
 
399
415
  ## Advanced Features
400
416
 
417
+ ### Theming
418
+
419
+ The editor supports dark and light themes. Set the `theme` option to switch between them:
420
+
421
+ ```javascript
422
+ // Light theme
423
+ showEditor(videoUrl, {
424
+ theme: 'light',
425
+ });
426
+
427
+ // Dark theme (default)
428
+ showEditor(videoUrl, {
429
+ theme: 'dark',
430
+ });
431
+ ```
432
+
433
+ | | Dark (default) | Light |
434
+ |---|---|---|
435
+ | **Background** | Black | White |
436
+ | **Icons & text** | White | Black |
437
+ | **Cancel/Save text** | White | Black |
438
+ | **Crop brackets & grid** | White | Black |
439
+ | **Progress indicator** | White | White |
440
+ | **Trimmer handle chevrons** | White | White |
441
+ | **Dialogs** | Dark style | Light style |
442
+
443
+ The `headerTextColor` and `handleIconColor` options automatically adapt to the active theme but can still be overridden explicitly.
444
+
401
445
  ### Audio Trimming
402
446
 
403
447
  <div align="center">
404
- <img src="images/audio_android.jpg" width="200" />
405
- <img src="images/audio_ios.jpg" width="200" />
448
+ <img src="images/audio_android.png" width="200" />
449
+ <img src="images/audio_ios.png" width="200" />
406
450
  </div>
407
451
 
408
- For audio-only trimming, specify the media type and output format:
452
+ For audio-only trimming, specify the media type and output format. The editor automatically displays an audio waveform visualization in place of the thumbnail track. The waveform updates on zoom for higher resolution.
409
453
 
410
454
  ```javascript
411
455
  showEditor(audioUrl, {
@@ -415,6 +459,20 @@ showEditor(audioUrl, {
415
459
  });
416
460
  ```
417
461
 
462
+ Customize the waveform appearance:
463
+
464
+ ```javascript
465
+ showEditor(audioUrl, {
466
+ type: 'audio',
467
+ outputExt: 'mp3',
468
+ waveformColor: '#FFFFFF',
469
+ waveformBackgroundColor: '#3478F6',
470
+ waveformBarWidth: 3,
471
+ waveformBarGap: 2,
472
+ waveformBarCornerRadius: 1.5,
473
+ });
474
+ ```
475
+
418
476
  ### Remote Files (HTTPS)
419
477
 
420
478
  To trim remote files, you need the HTTPS-enabled version of FFmpeg:
@@ -204,7 +204,7 @@ object VideoTrimmerUtil {
204
204
  endPosition: Long,
205
205
  callback: SingleCallback<Bitmap, Int>
206
206
  ) {
207
- BackgroundExecutor.execute(object : BackgroundExecutor.Task("", 0L, "") {
207
+ BackgroundExecutor.execute(object : BackgroundExecutor.Task("initial_thumbs", 0L, "") {
208
208
  override fun execute() {
209
209
  try {
210
210
  val interval = (endPosition - startPosition) / (totalThumbsCount - 1)
@@ -0,0 +1,92 @@
1
+ package com.videotrim.widgets
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.RectF
8
+ import android.util.AttributeSet
9
+ import android.view.View
10
+
11
+ /**
12
+ * Custom View that draws an audio waveform as a row of vertical rounded-rect bars.
13
+ *
14
+ * Each bar's height is driven by a normalized amplitude value in [0, 1].
15
+ * The view recalculates bar count from its own width and maps the amplitudes
16
+ * array proportionally, so it works correctly regardless of whether the
17
+ * amplitudes array has more or fewer entries than the visible bar count.
18
+ *
19
+ * The background color (set via [View.setBackgroundColor]) provides the
20
+ * waveform track color; the bars are drawn on top with [barColor].
21
+ */
22
+ class AudioWaveformView @JvmOverloads constructor(
23
+ context: Context,
24
+ attrs: AttributeSet? = null,
25
+ defStyleAttr: Int = 0
26
+ ) : View(context, attrs, defStyleAttr) {
27
+
28
+ var amplitudes: FloatArray = FloatArray(0)
29
+ private set
30
+ private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
31
+ color = Color.WHITE
32
+ style = Paint.Style.FILL
33
+ }
34
+ private val barRect = RectF()
35
+
36
+ var barColor: Int
37
+ get() = barPaint.color
38
+ set(value) {
39
+ barPaint.color = value
40
+ invalidate()
41
+ }
42
+
43
+ var barWidthPx: Float = 0f
44
+ set(value) {
45
+ field = value
46
+ invalidate()
47
+ }
48
+
49
+ var barGapPx: Float = 0f
50
+ set(value) {
51
+ field = value
52
+ invalidate()
53
+ }
54
+
55
+ var barCornerRadiusPx: Float = 0f
56
+ set(value) {
57
+ field = value
58
+ invalidate()
59
+ }
60
+
61
+ fun setAmplitudes(data: FloatArray) {
62
+ amplitudes = data
63
+ invalidate()
64
+ }
65
+
66
+ override fun onDraw(canvas: Canvas) {
67
+ super.onDraw(canvas)
68
+ if (amplitudes.isEmpty()) return
69
+
70
+ val totalHeight = height.toFloat()
71
+ val step = barWidthPx + barGapPx
72
+ if (step <= 0f) return
73
+ val barCount = (width.toFloat() / step).toInt()
74
+ if (barCount <= 0) return
75
+
76
+ // Keep bars from touching the container edges
77
+ val verticalPadding = barWidthPx * 1.5f
78
+ val drawableHeight = totalHeight - verticalPadding * 2f
79
+ if (drawableHeight <= 0f) return
80
+ val minBarHeight = barWidthPx
81
+
82
+ for (i in 0 until barCount) {
83
+ val ampIndex = (i * amplitudes.size / barCount).coerceIn(0, amplitudes.size - 1)
84
+ val amp = amplitudes[ampIndex]
85
+ val barHeight = (amp * drawableHeight).coerceAtLeast(minBarHeight)
86
+ val x = i * step
87
+ val y = verticalPadding + (drawableHeight - barHeight) / 2f
88
+ barRect.set(x, y, x + barWidthPx, y + barHeight)
89
+ canvas.drawRoundRect(barRect, barCornerRadiusPx, barCornerRadiusPx, barPaint)
90
+ }
91
+ }
92
+ }
@@ -6,8 +6,6 @@ import android.graphics.Color
6
6
  import android.graphics.Paint
7
7
  import android.graphics.Path
8
8
  import android.graphics.RectF
9
- import android.graphics.Region
10
- import android.os.Build
11
9
  import android.util.AttributeSet
12
10
  import android.util.TypedValue
13
11
  import android.view.MotionEvent
@@ -46,6 +44,16 @@ class CropOverlayView @JvmOverloads constructor(
46
44
  var onCropBegan: (() -> Unit)? = null
47
45
  var onCropEnded: (() -> Unit)? = null
48
46
 
47
+ var isLightTheme = false
48
+ set(value) {
49
+ field = value
50
+ val c = if (value) Color.BLACK else Color.WHITE
51
+ borderPaint.color = c
52
+ gridPaint.color = c
53
+ cornerPaint.color = c
54
+ invalidate()
55
+ }
56
+
49
57
  private val minCropSize = dpToPx(60f)
50
58
  private val borderWidth = dpToPx(1f)
51
59
  private val cornerLength = dpToPx(20f)
@@ -71,10 +79,6 @@ class CropOverlayView @JvmOverloads constructor(
71
79
  strokeCap = Paint.Cap.ROUND
72
80
  strokeJoin = Paint.Join.ROUND
73
81
  }
74
- private val dimmingPaint = Paint().apply {
75
- color = Color.argb(140, 0, 0, 0)
76
- style = Paint.Style.FILL
77
- }
78
82
 
79
83
  private enum class DragEdge {
80
84
  TOP, BOTTOM, LEFT, RIGHT,
@@ -128,8 +132,6 @@ class CropOverlayView @JvmOverloads constructor(
128
132
  if (cropRect.isEmpty) return
129
133
  val cr = cropRect
130
134
 
131
- drawDimming(canvas, cr)
132
-
133
135
  canvas.drawRect(cr, borderPaint)
134
136
 
135
137
  for (i in 1..2) {
@@ -163,22 +165,6 @@ class CropOverlayView @JvmOverloads constructor(
163
165
  canvas.drawPath(path, cornerPaint)
164
166
  }
165
167
 
166
- private fun drawDimming(canvas: Canvas, cr: RectF) {
167
- canvas.save()
168
- val fullPath = Path()
169
- fullPath.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
170
- val cropPath = Path()
171
- cropPath.addRect(cr, Path.Direction.CW)
172
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
173
- fullPath.op(cropPath, Path.Op.DIFFERENCE)
174
- canvas.drawPath(fullPath, dimmingPaint)
175
- } else {
176
- @Suppress("DEPRECATION")
177
- canvas.clipPath(cropPath, Region.Op.DIFFERENCE)
178
- canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), dimmingPaint)
179
- }
180
- canvas.restore()
181
- }
182
168
 
183
169
  override fun onTouchEvent(event: MotionEvent): Boolean {
184
170
  if (event.pointerCount > 1) {