react-native-video-trim 3.0.9 → 3.0.10

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
  - ✅ Save to Photos, Documents and Share to other apps
13
13
  - ✅ Check if file is valid video/audio
14
14
  - ✅ File operations: list, clean up, delete specific file
15
+ - ✅ Support React Native New + Old Arch
15
16
 
16
17
  <div align="left">
17
18
  <img src="images/document_picker.png" width="300" />
@@ -21,11 +22,19 @@
21
22
  ## Installation
22
23
 
23
24
  ```sh
24
- npm install react-native-video-trim
25
+ # new arch
26
+ npm install react-native-video-trim react-native-nitro-modules
27
+
28
+ # old arch
29
+ npm install react-native-video-trim@^3.0.0
25
30
 
26
31
  # or with yarn
27
32
 
28
- yarn add react-native-video-trim
33
+ # new arch
34
+ yarn add react-native-video-trim react-native-nitro-modules
35
+
36
+ # old arch
37
+ yarn add react-native-video-trim@^3.0.0
29
38
  ```
30
39
  ## For iOS (React Native CLI project)
31
40
  Run the following command to setup for iOS:
@@ -226,7 +235,6 @@ Main method to show Video Editor UI.
226
235
  - `alertOnFailTitle` (`default = "Error"`)
227
236
  - `alertOnFailMessage` (`default = "Fail to load media. Possibly invalid file or no network connection"`)
228
237
  - `alertOnFailCloseText` (`default = "Close"`)
229
- - `progressUpdateInterval` (`default = 0.1`): how fast the trimming progress update interval is, default is emit progress every 100ms (0.1 second)
230
238
 
231
239
  If `saveToPhoto = true`, you must ensure that you have request permission to write to photo/gallery
232
240
  - For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
@@ -274,7 +282,13 @@ Clean all generated output files in app storage. Return number of successfully d
274
282
  ## deleteFile()
275
283
  Delete a file in app storage. Return `true` if success
276
284
 
277
- # Events
285
+ # Callbacks (New arch)
286
+
287
+ ## showEditor
288
+
289
+ ## closeEditor
290
+
291
+ # Events (Old arch)
278
292
  To listen for events you interest, do the following:
279
293
  ```js
280
294
  useEffect(() => {
@@ -94,6 +94,7 @@ dependencies {
94
94
  //noinspection GradleDynamicVersion
95
95
  implementation "com.facebook.react:react-native:+"
96
96
  implementation 'androidx.recyclerview:recyclerview:1.3.1'
97
+ implementation 'io.github.maitrungduc1410:ffmpeg-kit-min:6.0.0'
97
98
  }
98
99
 
99
100
  if (isNewArchitectureEnabled()) {
@@ -99,7 +99,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
99
99
  private boolean openShareSheetOnFinish = false;
100
100
  private boolean isVideoType = true;
101
101
  private boolean closeWhenFinish = true;
102
- private float progressUpdateInterval = 0.1f;
103
102
 
104
103
  private static final int REQUEST_CODE_SAVE_FILE = 1;
105
104
 
@@ -195,7 +194,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
195
194
  isVideoType = !config.hasKey("type") || !Objects.equals(config.getString("type"), "audio");
196
195
 
197
196
  closeWhenFinish = !config.hasKey("closeWhenFinish") || config.getBoolean("closeWhenFinish");
198
- progressUpdateInterval = config.hasKey("progressUpdateInterval") ? (float) config.getDouble("progressUpdateInterval") : 0.1f;
199
197
 
200
198
  Activity activity = getReactApplicationContext().getCurrentActivity();
201
199
 
@@ -374,6 +372,11 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
374
372
  alertDialog.show();
375
373
  }
376
374
 
375
+ @Override
376
+ public void onLog(WritableMap log) {
377
+ sendEvent(getReactApplicationContext(), "onLog", log);
378
+ }
379
+
377
380
  @Override
378
381
  public void onStatistics(WritableMap statistics) {
379
382
  sendEvent(getReactApplicationContext(), "onStatistics", statistics);
@@ -471,7 +474,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
471
474
  sendEvent(getReactApplicationContext(), "onStartTrimming", null);
472
475
 
473
476
  if (trimmerView != null) {
474
- trimmerView.onSaveClicked(progressUpdateInterval);
477
+ trimmerView.onSaveClicked();
475
478
  }
476
479
  });
477
480
 
@@ -559,9 +562,9 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
559
562
  private void isValidFile(String filePath, Promise promise) {
560
563
  MediaMetadataUtil.checkFileValidity(filePath, (isValid, fileType, duration) -> {
561
564
  if (isValid) {
562
- System.out.println("Valid " + fileType + " file with duration: " + duration + " milliseconds");
565
+ Log.d(TAG, "Valid " + fileType + " file with duration: " + duration + " milliseconds");
563
566
  } else {
564
- System.out.println("Invalid file");
567
+ Log.d(TAG, "Invalid file");
565
568
  }
566
569
 
567
570
  WritableMap map = Arguments.createMap();
@@ -11,5 +11,6 @@ public interface VideoTrimListener {
11
11
  void onError(String errorMessage, ErrorCode errorCode);
12
12
  void onCancel();
13
13
  void onSave();
14
+ void onLog(WritableMap log);
14
15
  void onStatistics(WritableMap statistics);
15
16
  }
@@ -1,21 +1,27 @@
1
1
  package com.videotrim.utils;
2
2
 
3
+ import android.annotation.SuppressLint;
3
4
  import android.graphics.Bitmap;
4
- import android.media.MediaCodec;
5
5
  import android.media.MediaMetadataRetriever;
6
+ import android.util.Log;
7
+
8
+ import com.arthenica.ffmpegkit.FFmpegKit;
9
+ import com.arthenica.ffmpegkit.FFmpegSession;
10
+ import com.arthenica.ffmpegkit.ReturnCode;
11
+ import com.arthenica.ffmpegkit.SessionState;
6
12
  import com.facebook.react.bridge.Arguments;
7
13
  import com.facebook.react.bridge.WritableMap;
8
14
  import com.videotrim.enums.ErrorCode;
9
15
  import com.videotrim.interfaces.VideoTrimListener;
16
+
17
+ import java.text.SimpleDateFormat;
18
+ import java.util.Date;
19
+ import java.util.TimeZone;
20
+
10
21
  import iknow.android.utils.DeviceUtil;
11
22
  import iknow.android.utils.UnitConverter;
12
23
  import iknow.android.utils.callback.SingleCallback;
13
24
  import iknow.android.utils.thread.BackgroundExecutor;
14
- import android.media.MediaExtractor;
15
- import android.media.MediaFormat;
16
- import android.media.MediaMuxer;
17
- import java.io.IOException;
18
- import java.nio.ByteBuffer;
19
25
 
20
26
  public class VideoTrimmerUtil {
21
27
 
@@ -36,201 +42,80 @@ public class VideoTrimmerUtil {
36
42
  public static final int THUMB_WIDTH = UnitConverter.dpToPx(25); // x2 for better resolution
37
43
  private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
38
44
 
39
- private static final int BUFFER_SIZE = 1024 * 1024; // 1MB buffer
40
-
41
- // Custom session class to manage trimming and cancellation
42
- public static class TrimSession {
43
- private final Thread trimThread;
44
- private volatile boolean isCancelled = false;
45
-
46
- private TrimSession(Thread thread) {
47
- this.trimThread = thread;
48
- }
49
-
50
- public void cancel() {
51
- isCancelled = true;
52
- if (trimThread != null) {
53
- trimThread.interrupt();
45
+ public static FFmpegSession trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, final VideoTrimListener callback) {
46
+ // Get the current date and time
47
+ Date currentDate = new Date();
48
+
49
+ // Create a SimpleDateFormat object with the desired format
50
+ @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
51
+
52
+ // Set the timezone to UTC
53
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
54
+ // Format the current date and time
55
+ String formattedDateTime = dateFormat.format(currentDate);
56
+
57
+ String[] cmds = {
58
+ "-ss",
59
+ startMs + "ms",
60
+ "-to",
61
+ endMs + "ms",
62
+ "-i",
63
+ inputFile,
64
+ "-c",
65
+ "copy",
66
+ "-metadata",
67
+ "creation_time=" + formattedDateTime,
68
+ outputFile
69
+ };
70
+ Log.d(TAG,"Command111: " + String.join(",", cmds));
71
+
72
+ FFmpegSession s = FFmpegKit.execute("-protocols");
73
+ Log.d(TAG, "1111getOutput: " + s.getOutput());
74
+ Log.d(TAG, "1111getAllLogs: " + s.getAllLogs());
75
+
76
+ return FFmpegKit.executeWithArgumentsAsync(cmds, session -> {
77
+ SessionState state = session.getState();
78
+ ReturnCode returnCode = session.getReturnCode();
79
+ if (ReturnCode.isSuccess(session.getReturnCode())) {
80
+ // SUCCESS
81
+ callback.onFinishTrim(outputFile, startMs, endMs, videoDuration);
82
+ } else if (ReturnCode.isCancel(session.getReturnCode())) {
83
+ // CANCEL
84
+ callback.onCancelTrim();
85
+ } else {
86
+ // FAILURE
87
+ String errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
88
+ callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED);
54
89
  }
55
- }
56
-
57
- public boolean isActive() {
58
- return !isCancelled;
59
- }
60
- }
61
-
62
- public static TrimSession trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, final VideoTrimListener callback, float progressUpdateInterval) {
63
- // Start trimming in a background thread
64
- Thread trimThread = new Thread(() -> {
65
- MediaExtractor extractor = null;
66
- MediaMuxer muxer = null;
67
- TrimSession session = new TrimSession(Thread.currentThread());
68
-
69
- try {
70
- // Get rotation metadata from input file
71
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
72
- retriever.setDataSource(inputFile);
73
- String rotationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
74
- int rotation = rotationStr != null ? Integer.parseInt(rotationStr) : 0;
75
-
76
-
77
- extractor = new MediaExtractor();
78
- extractor.setDataSource(inputFile);
79
-
80
- int trackCount = extractor.getTrackCount();
81
- muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
82
- int[] trackIndices = new int[trackCount];
83
- long[] trackStartTimes = new long[trackCount];
84
- boolean[] tracksAdded = new boolean[trackCount];
85
- int videoTrackIndex = -1;
86
-
87
- // Calculate trimmed duration
88
- long startUs = startMs * 1000; // e.g., 5s = 5000000us
89
- long endUs = endMs * 1000; // e.g., 9s = 9000000us
90
- long trimmedDurationUs = endUs - startUs; // e.g., 4s = 4000000us
91
-
92
- // Determine max buffer size from video format and resolution
93
- int maxBufferSize = BUFFER_SIZE; // Default 1MB
94
- String widthStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
95
- String heightStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
96
- int width = widthStr != null ? Integer.parseInt(widthStr) : 0; // Default to unknown
97
- int height = heightStr != null ? Integer.parseInt(heightStr) : 0;
98
-
99
- // IMPORTANT: after this line, the retriever is no longer needed
100
- retriever.release();
101
-
102
- // Adjust buffer size based on resolution
103
- if (width > 3840 || height > 2160) { // 8K+
104
- maxBufferSize = 16 * 1024 * 1024; // 16MB for 8K
105
- } else if (width > 1920 || height > 1080) { // 4K
106
- maxBufferSize = 8 * 1024 * 1024; // 8MB for 4K
107
- } else if (width > 1280 || height > 720) { // 1080p
108
- maxBufferSize = 4 * 1024 * 1024; // 4MB for 1080p
109
- } // 720p or lower (or unknown resolution) sticks with BUFFER_SIZE (1MB)
110
-
111
- // Add tracks with corrected duration
112
- for (int i = 0; i < trackCount; i++) {
113
- MediaFormat format = extractor.getTrackFormat(i);
114
-
115
- // Override with KEY_MAX_INPUT_SIZE if available
116
- if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
117
- int maxInputSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
118
- maxBufferSize = Math.max(maxBufferSize, maxInputSize);
119
- }
120
-
121
- String mime = format.getString(MediaFormat.KEY_MIME);
122
- if (mime != null && mime.startsWith("video/")) {
123
- videoTrackIndex = i;
124
- }
125
- // Set the duration for each track to the trimmed duration
126
- format.setLong(MediaFormat.KEY_DURATION, trimmedDurationUs);
127
- trackIndices[i] = muxer.addTrack(format);
128
- tracksAdded[i] = false;
129
- extractor.selectTrack(i);
130
- }
131
-
132
- // Set rotation metadata on muxer
133
- muxer.setOrientationHint(rotation);
134
-
135
- // Seek to start time
136
- extractor.seekTo(startUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
137
-
138
- ByteBuffer buffer = ByteBuffer.allocate(maxBufferSize);
139
- MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
140
- long lastProgressTime = System.currentTimeMillis();
141
- boolean videoSampleWritten = false;
142
-
143
- muxer.start();
144
- while (session.isActive()) {
145
- bufferInfo.size = extractor.readSampleData(buffer, 0);
146
- if (bufferInfo.size < 0) break; // EOS
147
-
148
- long sampleTime = extractor.getSampleTime();
149
- if (sampleTime > endUs) break;
150
-
151
- bufferInfo.presentationTimeUs = sampleTime;
152
- int extractorFlags = extractor.getSampleFlags();
153
- bufferInfo.flags = (extractorFlags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0 ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
154
- int trackIndex = extractor.getSampleTrackIndex();
155
-
156
- if (!tracksAdded[trackIndex]) {
157
- trackStartTimes[trackIndex] = sampleTime;
158
- tracksAdded[trackIndex] = true;
159
- }
160
- bufferInfo.presentationTimeUs -= trackStartTimes[trackIndex]; // Adjust time to start at 0
161
-
162
- // Ensure presentation time doesn't exceed trimmed duration
163
- if (bufferInfo.presentationTimeUs >= trimmedDurationUs) {
164
- extractor.advance();
165
- continue;
166
- }
167
-
168
- if (trackIndex == videoTrackIndex) {
169
- videoSampleWritten = true;
170
- }
171
- muxer.writeSampleData(trackIndices[trackIndex], buffer, bufferInfo);
172
- extractor.advance();
173
-
174
- long currentTime = System.currentTimeMillis();
175
- if (currentTime - lastProgressTime >= progressUpdateInterval * 1000) {
176
- double progress = (double)(sampleTime - startUs) / (endUs - startUs);
177
- if (progress > 0 && progress <= 1) {
178
- WritableMap statsMap = Arguments.createMap();
179
- statsMap.putDouble("time", progress * videoDuration);
180
- statsMap.putDouble("progress", progress); // Percentage
181
-
182
- callback.onStatistics(statsMap);
183
- callback.onTrimmingProgress((int)(progress * 100));
184
- }
185
- lastProgressTime = currentTime;
186
- }
187
- }
188
-
189
- // For static videos: ensure one video frame if none written
190
- if (!videoSampleWritten && videoTrackIndex != -1) {
191
- for (int i = 0; i < trackCount; i++) {
192
- if (i != videoTrackIndex) {
193
- extractor.unselectTrack(i);
194
- }
195
- }
196
- extractor.selectTrack(videoTrackIndex);
197
- extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
198
- bufferInfo.size = extractor.readSampleData(buffer, 0);
199
- if (bufferInfo.size >= 0) {
200
- bufferInfo.presentationTimeUs = 0;
201
- // Map MediaExtractor flags to MediaCodec flags
202
- int extractorFlags = extractor.getSampleFlags();
203
- bufferInfo.flags = (extractorFlags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0 ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0;
204
- muxer.writeSampleData(trackIndices[videoTrackIndex], buffer, bufferInfo);
205
- }
206
- }
207
-
208
- if (session.isActive()) {
209
- muxer.stop();
210
- callback.onFinishTrim(outputFile, startMs, endMs, videoDuration);
211
- } else {
212
- callback.onCancelTrim();
213
- }
214
-
215
- } catch (IOException e) {
216
- e.printStackTrace();
217
- callback.onError("Trimming failed: " + e.getMessage(), ErrorCode.TRIMMING_FAILED);
218
- } finally {
219
- try {
220
- if (muxer != null && session.isActive()) {
221
- muxer.release();
222
- }
223
- if (extractor != null) extractor.release();
224
- } catch (Exception e) {
225
- e.printStackTrace();
226
- System.err.println("Error releasing resources: " + e.getMessage());
227
- }
90
+ }, log -> {
91
+ Log.d(TAG, "FFmpeg process started with log " + log.getMessage());
92
+
93
+ WritableMap map = Arguments.createMap();
94
+ map.putInt("level", log.getLevel().getValue());
95
+ map.putString("message", log.getMessage());
96
+ map.putDouble("sessionId", log.getSessionId());
97
+ map.putString("logStr", log.toString());
98
+ callback.onLog(map);
99
+ }, statistics -> {
100
+ int timeInMilliseconds = (int) statistics.getTime();
101
+ if (timeInMilliseconds > 0) {
102
+ int completePercentage =
103
+ (timeInMilliseconds * 100) / videoDuration;
104
+ callback.onTrimmingProgress(Math.min(Math.max(completePercentage, 0), 100));
228
105
  }
229
- });
230
106
 
231
- TrimSession session = new TrimSession(trimThread);
232
- trimThread.start();
233
- return session;
107
+ WritableMap map = Arguments.createMap();
108
+ map.putDouble("sessionId", statistics.getSessionId());
109
+ map.putInt("videoFrameNumber", statistics.getVideoFrameNumber());
110
+ map.putDouble("videoFps", statistics.getVideoFps());
111
+ map.putDouble("videoQuality", statistics.getVideoQuality());
112
+ map.putDouble("size", statistics.getSize());
113
+ map.putDouble("time", statistics.getTime());
114
+ map.putDouble("bitrate", statistics.getBitrate());
115
+ map.putDouble("speed", statistics.getSpeed());
116
+ map.putString("statisticsStr", statistics.toString());
117
+ callback.onStatistics(map);
118
+ });
234
119
  }
235
120
 
236
121
  public static void shootVideoThumbInBackground(final MediaMetadataRetriever mediaMetadataRetriever, final int totalThumbsCount, final long startPosition,
@@ -48,6 +48,7 @@ import java.util.Locale;
48
48
  import iknow.android.utils.DeviceUtil;
49
49
  import iknow.android.utils.thread.BackgroundExecutor;
50
50
  import iknow.android.utils.thread.UiThreadExecutor;
51
+ import com.arthenica.ffmpegkit.FFmpegSession;
51
52
 
52
53
  public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
53
54
 
@@ -108,7 +109,7 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
108
109
  private long jumpToPositionOnLoad = 0;
109
110
  private FrameLayout headerView;
110
111
  private TextView headerText;
111
- private VideoTrimmerUtil.TrimSession trimSession;
112
+ private FFmpegSession ffmpegSession;
112
113
  private boolean alertOnFailToLoad = true;
113
114
  private String alertOnFailTitle = "Error";
114
115
  private String alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection";
@@ -365,22 +366,21 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
365
366
  setHandleTouchListener(trailingHandle, false);
366
367
  }
367
368
 
368
- public void onSaveClicked(float progressUpdateInterval) {
369
+ public void onSaveClicked() {
369
370
  onMediaPause();
370
- trimSession = VideoTrimmerUtil.trim(
371
+ ffmpegSession = VideoTrimmerUtil.trim(
371
372
  mSourceUri.toString(),
372
373
  StorageUtil.getOutputPath(mContext, mOutputExt),
373
374
  mDuration,
374
375
  startTime,
375
376
  endTime,
376
- mOnTrimVideoListener,
377
- progressUpdateInterval
377
+ mOnTrimVideoListener
378
378
  );
379
379
  }
380
380
 
381
381
  public void onCancelTrimClicked() {
382
- if (trimSession != null) {
383
- trimSession.cancel();
382
+ if (ffmpegSession != null) {
383
+ ffmpegSession.cancel();
384
384
  } else {
385
385
  mOnTrimVideoListener.onCancelTrim();
386
386
  }