react-native-video-trim 2.1.0 → 2.2.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
@@ -9,6 +9,14 @@
9
9
  ## Features
10
10
  - ✅ Support video and audio
11
11
  - ✅ Support local and remote files
12
+ - ✅ Save to Photos, Documents and Share to other apps
13
+ - ✅ Check if file is valid video/audio
14
+ - ✅ File operations: list, clean up, delete specific file
15
+
16
+ <div align="left">
17
+ <img src="images/document_picker.png" width="300" />
18
+ <img src="images/share_sheet.png" width="300" />
19
+ </div>
12
20
 
13
21
  ## Installation
14
22
 
@@ -76,6 +84,11 @@ export default function App() {
76
84
  const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
77
85
  const subscription = eventEmitter.addListener('VideoTrim', (event) => {
78
86
  switch (event.name) {
87
+ case 'onLoad': {
88
+ // on media loaded successfully
89
+ console.log('onLoadListener', event);
90
+ break;
91
+ }
79
92
  case 'onShow': {
80
93
  console.log('onShowListener', event);
81
94
  break;
@@ -96,10 +109,22 @@ export default function App() {
96
109
  console.log('onCancelTrimming', event);
97
110
  break;
98
111
  }
112
+ case 'onCancel': {
113
+ console.log('onCancel', event);
114
+ break;
115
+ }
99
116
  case 'onError': {
100
117
  console.log('onError', event);
101
118
  break;
102
119
  }
120
+ case 'onLog': {
121
+ console.log('onLog', event);
122
+ break;
123
+ }
124
+ case 'onStatistics': {
125
+ console.log('onStatistics', event);
126
+ break;
127
+ }
103
128
  }
104
129
  });
105
130
 
@@ -161,41 +186,83 @@ Main method to show Video Editor UI.
161
186
 
162
187
  *Params*:
163
188
  - `videoPath`: Path to video file, if this is an invalid path, `onError` event will be fired
164
- - `config` (optional):
189
+ - `config` (optional, every sub props of `config` is optional):
165
190
 
166
- - `type` (optional, `default = video`): which player to use, `video` or `audio`
167
- - `outputExt` (optional, `default = mp4`): output file extension
168
- - `enableHapticFeedback` (optional, `default = true`): whether to enable haptic feedback
169
- - `saveToPhoto` (optional, Video-only, `default = false`): whether to save video to photo/gallery after editing
170
- - `openDocumentsOnFinish` (optional, `default = false`): open Document Picker on done trimming
171
- - `openShareSheetOnFinish` (optional, `default = false`): open Share Sheet on done trimming
172
- - `removeAfterSavedToPhoto` (optional, `default = false`): whether to remove output file from storage after saved to Photo successfully
173
- - `removeAfterFailedToSavePhoto` (optional, `default = false`): whether to remove output file if fail to save to Photo
174
- - `removeAfterSavedToDocuments` (optional, `default = false`): whether to remove output file from storage after saved Documents successfully
175
- - `removeAfterFailedToSaveDocuments` (optional, `default = false`): whether to remove output file from storage after fail to save to Documents
176
- - `removeAfterShared` (optional, `default = false`): whether to remove output file from storage after saved Share successfully. iOS only, on Android you'll have to manually remove the file (this is because on Android there's no way to detect when sharing is successful)
177
- - `removeAfterFailedToShare` (optional, `default = false`): whether to remove output file from storage after fail to Share. iOS only, on Android you'll have to manually remove the file
191
+ - `type` (`default = video`): which player to use, `video` or `audio`
192
+ - `outputExt` (`default = mp4`): output file extension
193
+ - `enableHapticFeedback` (`default = true`): whether to enable haptic feedback
194
+ - `saveToPhoto` (Video-only, `default = false`): whether to save video to photo/gallery after editing
195
+ - `openDocumentsOnFinish` (`default = false`): open Document Picker on done trimming
196
+ - `openShareSheetOnFinish` (`default = false`): open Share Sheet on done trimming
197
+ - `removeAfterSavedToPhoto` (`default = false`): whether to remove output file from storage after saved to Photo successfully
198
+ - `removeAfterFailedToSavePhoto` (`default = false`): whether to remove output file if fail to save to Photo
199
+ - `removeAfterSavedToDocuments` (`default = false`): whether to remove output file from storage after saved Documents successfully
200
+ - `removeAfterFailedToSaveDocuments` (`default = false`): whether to remove output file from storage after fail to save to Documents
201
+ - `removeAfterShared` (`default = false`): whether to remove output file from storage after saved Share successfully. iOS only, on Android you'll have to manually remove the file (this is because on Android there's no way to detect when sharing is successful)
202
+ - `removeAfterFailedToShare` (`default = false`): whether to remove output file from storage after fail to Share. iOS only, on Android you'll have to manually remove the file
178
203
  - `maxDuration` (optional): maximum duration for the trimmed video
179
- - `minDuration` (optional): minimum duration for the trimmed video
180
- - `cancelButtonText` (optional): text of left button in Editor dialog
181
- - `saveButtonText` (optional): text of right button in Editor dialog
182
- - `enableCancelDialog` (optional, `default = true`): whether to show alert dialog on press Cancel
183
- - `cancelDialogTitle` (optional, `default = "Warning!"`)
184
- - `cancelDialogMessage` (optional, `default = "Are you sure want to cancel?"`)
185
- - `cancelDialogCancelText` (optional, `default = "Close"`)
186
- - `cancelDialogConfirmText` (optional, `default = "Proceed"`)
187
- - `enableSaveDialog` (optional, `default = true`): whether to show alert dialog on press Save
188
- - `saveDialogTitle` (optional, `default = "Confirmation!"`)
189
- - `saveDialogMessage` (optional, `default = "Are you sure want to save?"`)
190
- - `saveDialogCancelText` (optional, `default = "Close"`)
191
- - `saveDialogConfirmText` (optional, `default = "Proceed"`)
192
- - `fullScreenModalIOS` (optional, `default = false`): whether to open editor in fullscreen modal
193
- - `trimmingText` (optional, `default = "Trimming video..."`): trimming text on the progress dialog
204
+ - `minDuration` (`default = 1000`): minimum duration for the trimmed video
205
+ - `cancelButtonText` (`default= "Cancel"`): text of left button in Editor dialog
206
+ - `saveButtonText` (`default= "Save"`): text of right button in Editor dialog
207
+ - `enableCancelDialog` (`default = true`): whether to show alert dialog on press Cancel
208
+ - `cancelDialogTitle` (`default = "Warning!"`)
209
+ - `cancelDialogMessage` (`default = "Are you sure want to cancel?"`)
210
+ - `cancelDialogCancelText` (`default = "Close"`)
211
+ - `cancelDialogConfirmText` (`default = "Proceed"`)
212
+ - `enableSaveDialog` (`default = true`): whether to show alert dialog on press Save
213
+ - `saveDialogTitle` (`default = "Confirmation!"`)
214
+ - `saveDialogMessage` (`default = "Are you sure want to save?"`)
215
+ - `saveDialogCancelText` (`default = "Close"`)
216
+ - `saveDialogConfirmText` (`default = "Proceed"`)
217
+ - `fullScreenModalIOS` (`default = false`): whether to open editor in fullscreen modal
218
+ - `trimmingText` (`default = "Trimming video..."`): trimming text on the progress dialog
219
+ - `autoplay` (`default = false`): whether to autoplay media on load
220
+ - `jumpToPositionOnLoad` (optional): which time position should jump on media loaded (millisecond)
221
+ - `closeWhenFinish` (`default = true`): should editor close on finish trimming
222
+ - `enableCancelTrimming` (`default = true`): enable cancel trimming
223
+ - `cancelTrimmingButtonText` (`default = "Cancel"`)
224
+ - `enableCancelTrimmingDialog` (`default = true`)
225
+ - `cancelTrimmingDialogTitle` (`default = "Warning!"`)
226
+ - `cancelTrimmingDialogMessage` (`default = "Are you sure want to cancel trimming?"`)
227
+ - `cancelTrimmingDialogCancelText` (`default = "Close"`)
228
+ - `cancelTrimmingDialogConfirmText` (`default = "Proceed"`)
229
+ - `headerText` (optional)
230
+ - `headerTextSize` (`default = 16`)
231
+ - `headerTextColor` (`default = white`)
232
+ - `alertOnFailToLoad` (`default = true`)
233
+ - `alertOnFailTitle` (`default = "Error"`)
234
+ - `alertOnFailMessage` (`default = "Fail to load media. Possibly invalid file or no network connection"`)
235
+ - `alertOnFailCloseText` (`default = "Close"`)
194
236
 
195
237
  If `saveToPhoto = true`, you must ensure that you have request permission to write to photo/gallery
196
238
  - For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
197
239
  - For iOS: you need `NSPhotoLibraryUsageDescription` in Info.plist
198
240
 
241
+ If `openShareSheetOnFinish=true`, on Android you'll need to update `AndroidManifest.xml` like below:
242
+ ```xml
243
+ </application>
244
+ ...
245
+ <provider
246
+ android:name="androidx.core.content.FileProvider"
247
+ android:authorities="${applicationId}.provider"
248
+ android:exported="false"
249
+ android:grantUriPermissions="true">
250
+ <meta-data
251
+ android:name="android.support.FILE_PROVIDER_PATHS"
252
+ android:resource="@xml/file_paths" />
253
+ </provider>
254
+ </application>
255
+ ```
256
+
257
+ If you face issue when building Android app related to `file_paths`, then you may need to create `res/xml/file_paths.xml`: with the following content:
258
+ ```xml
259
+ <?xml version="1.0" encoding="utf-8"?>
260
+ <paths xmlns:android="http://schemas.android.com/apk/res/android">
261
+ <files-path name="internal_files" path="." />
262
+ <external-path name="external_files" path="." />
263
+ </paths>
264
+ ```
265
+
199
266
  ## isValidFile(videoPath: string)
200
267
 
201
268
  This method is to check if a path is a valid video/audio
@@ -220,43 +287,43 @@ useEffect(() => {
220
287
  const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
221
288
  const subscription = eventEmitter.addListener('VideoTrim', (event) => {
222
289
  switch (event.name) {
290
+ case 'onLoad': {
291
+ console.log('onLoadListener', event);
292
+ break;
293
+ }
223
294
  case 'onShow': {
224
- // on Dialog show
225
295
  console.log('onShowListener', event);
226
296
  break;
227
297
  }
228
298
  case 'onHide': {
229
- // on Dialog hide
230
299
  console.log('onHide', event);
231
300
  break;
232
301
  }
233
302
  case 'onStartTrimming': {
234
- // on start trimming
235
303
  console.log('onStartTrimming', event);
236
304
  break;
237
305
  }
238
306
  case 'onFinishTrimming': {
239
- // on trimming is done
240
307
  console.log('onFinishTrimming', event);
241
308
  break;
242
309
  }
243
310
  case 'onCancelTrimming': {
244
- // when user clicks Cancel button
245
311
  console.log('onCancelTrimming', event);
246
312
  break;
247
313
  }
314
+ case 'onCancel': {
315
+ console.log('onCancel', event);
316
+ break;
317
+ }
248
318
  case 'onError': {
249
- // any error occured: invalid file, lack of permissions to write to photo/gallery, unexpected error...
250
319
  console.log('onError', event);
251
320
  break;
252
321
  }
253
322
  case 'onLog': {
254
- // FFMPEG logs (while trimming)
255
323
  console.log('onLog', event);
256
324
  break;
257
325
  }
258
326
  case 'onStatistics': {
259
- // FFMPEG stats (while trimming)
260
327
  console.log('onStatistics', event);
261
328
  break;
262
329
  }
@@ -268,6 +335,56 @@ useEffect(() => {
268
335
  };
269
336
  }, []);
270
337
  ```
338
+ # Audio support
339
+ <div align="left">
340
+ <img src="images/audio_android.jpg" width="200" />
341
+ <img src="images/audio_ios.jpg" width="200" />
342
+ </div>
343
+
344
+ For audio only you have to pass `type=audio` and `outputExt`:
345
+ ```ts
346
+ showEditor(url, {
347
+ type: 'audio', // important
348
+ outputExt: 'wav', // important: any audio type for output file extension
349
+ })
350
+ ```
351
+
352
+ You must install FFMPEG version from "https" onwards, "min" version won't work. Eg.
353
+ ```gradle
354
+ // Android: android/build.gradle > buildscript > ext
355
+
356
+ buildscript {
357
+ ext {
358
+ ffmpegKitPackage = "full"
359
+ }
360
+
361
+ ---
362
+ // iOS:
363
+
364
+ FFMPEGKIT_PACKAGE=https npx pod-install ios
365
+
366
+ // or
367
+
368
+ FFMPEGKIT_PACKAGE=https pod install
369
+ ```
370
+
371
+ # Cancel trimming
372
+ <div align="left">
373
+ <img src="images/progress.jpg" width="200" />
374
+ <img src="images/cancel_confirm.jpg" width="200" />
375
+ </div>
376
+
377
+ While trimming, you can press Cancel to terminate the process.
378
+
379
+ Related props: `enableCancelTrimming, cancelTrimmingButtonText, enableCancelTrimmingDialog, cancelTrimmingDialogTitle, cancelTrimmingDialogMessage, cancelTrimmingDialogCancelText, cancelTrimmingDialogConfirmText`
380
+
381
+ # Fail to load media
382
+ <img src="images/fail_to_load_media.jpg" width="200" />
383
+
384
+ If there's error while loading media, there'll be a prompt
385
+
386
+ Related props: `alertOnFailToLoad, alertOnFailTitle, alertOnFailMessage, alertOnFailCloseText`
387
+
271
388
  # FFMPEG Version
272
389
  This library uses FFMPEG-Kit Android under the hood, by default FFMPEG-min is used, which gives smallest bundle size: https://github.com/arthenica/ffmpeg-kit#9-packages
273
390
 
@@ -12,8 +12,10 @@ import android.graphics.Color;
12
12
  import android.net.Uri;
13
13
  import android.os.Build;
14
14
  import android.util.Log;
15
+ import android.util.TypedValue;
15
16
  import android.view.Gravity;
16
17
  import android.view.ViewGroup;
18
+ import android.widget.Button;
17
19
  import android.widget.LinearLayout;
18
20
  import android.widget.ProgressBar;
19
21
  import android.widget.TextView;
@@ -21,6 +23,7 @@ import android.widget.TextView;
21
23
  import androidx.annotation.NonNull;
22
24
  import androidx.annotation.Nullable;
23
25
  import androidx.appcompat.app.AlertDialog;
26
+ import androidx.core.content.ContextCompat;
24
27
  import androidx.core.content.FileProvider;
25
28
 
26
29
  import com.facebook.react.bridge.ActivityEventListener;
@@ -62,10 +65,17 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
62
65
  private VideoTrimmerView trimmerView;
63
66
  private AlertDialog alertDialog;
64
67
  private AlertDialog mProgressDialog;
68
+ private AlertDialog cancelTrimmingConfirmDialog;
65
69
  private ProgressBar mProgressBar;
66
70
  private int listenerCount = 0;
67
-
68
-
71
+ private boolean enableCancelTrimming = true;
72
+
73
+ private String cancelTrimmingButtonText = "Cancel";
74
+ private boolean enableCancelTrimmingDialog = true;
75
+ private String cancelTrimmingDialogTitle = "Warning!";
76
+ private String cancelTrimmingDialogMessage = "Are you sure want to cancel trimming?";
77
+ private String cancelTrimmingDialogCancelText = "Close";
78
+ private String cancelTrimmingDialogConfirmText = "Proceed";
69
79
  private boolean enableCancelDialog = true;
70
80
  private String cancelDialogTitle = "Warning!";
71
81
  private String cancelDialogMessage = "Are you sure want to cancel?";
@@ -83,11 +93,12 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
83
93
  private boolean removeAfterFailedToSavePhoto = false;
84
94
  private boolean removeAfterSavedToDocuments = false;
85
95
  private boolean removeAfterFailedToSaveDocuments = false;
86
- private boolean removeAfterShared = false; // TODO: on Android there's no way to know if user shared the file or share sheet closed
87
- private boolean removeAfterFailedToShare = false; // TODO: implement this
96
+ // private boolean removeAfterShared = false; // TODO: on Android there's no way to know if user shared the file or share sheet closed
97
+ // private boolean removeAfterFailedToShare = false; // TODO: implement this
88
98
  private boolean openDocumentsOnFinish = false;
89
99
  private boolean openShareSheetOnFinish = false;
90
100
  private boolean isVideoType = true;
101
+ private boolean closeWhenFinish = true;
91
102
 
92
103
  private static final int REQUEST_CODE_SAVE_FILE = 1;
93
104
 
@@ -128,7 +139,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
128
139
  StorageUtil.deleteFile(outputFile);
129
140
  }
130
141
  } finally {
131
- hideDialog();
142
+ hideDialog(true);
132
143
  }
133
144
  }
134
145
  }
@@ -148,6 +159,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
148
159
  if (trimmerView != null || alertDialog != null) {
149
160
  return;
150
161
  }
162
+ enableCancelTrimming = !config.hasKey("enableCancelTrimming") || config.getBoolean("enableCancelTrimming");
163
+
164
+ cancelTrimmingButtonText = config.hasKey("cancelTrimmingButtonText") ? config.getString("cancelTrimmingButtonText") : "Cancel";
165
+ enableCancelTrimmingDialog = !config.hasKey("enableCancelTrimmingDialog") || config.getBoolean("enableCancelTrimmingDialog");
166
+ cancelTrimmingDialogTitle = config.hasKey("cancelTrimmingDialogTitle") ? config.getString("cancelTrimmingDialogTitle") : "Warning!";
167
+ cancelTrimmingDialogMessage = config.hasKey("cancelTrimmingDialogMessage") ? config.getString("cancelTrimmingDialogMessage") : "Are you sure want to cancel trimming?";
168
+ cancelTrimmingDialogCancelText = config.hasKey("cancelTrimmingDialogCancelText") ? config.getString("cancelTrimmingDialogCancelText") : "Close";
169
+ cancelTrimmingDialogConfirmText = config.hasKey("cancelTrimmingDialogConfirmText") ? config.getString("cancelTrimmingDialogConfirmText") : "Proceed";
151
170
 
152
171
  enableCancelDialog = !config.hasKey("enableCancelDialog") || config.getBoolean("enableCancelDialog");
153
172
  cancelDialogTitle = config.hasKey("cancelDialogTitle") ? config.getString("cancelDialogTitle") : "Warning!";
@@ -167,13 +186,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
167
186
  removeAfterFailedToSavePhoto = config.hasKey("removeAfterFailedToSavePhoto") && config.getBoolean("removeAfterFailedToSavePhoto");
168
187
  removeAfterSavedToDocuments = config.hasKey("removeAfterSavedToDocuments") && config.getBoolean("removeAfterSavedToDocuments");
169
188
  removeAfterFailedToSaveDocuments = config.hasKey("removeAfterFailedToSaveDocuments") && config.getBoolean("removeAfterFailedToSaveDocuments");
170
- removeAfterShared = config.hasKey("removeAfterShared") && config.getBoolean("removeAfterShared");
171
- removeAfterFailedToShare = config.hasKey("removeAfterFailedToShare") && config.getBoolean("removeAfterFailedToShare");
189
+ // removeAfterShared = config.hasKey("removeAfterShared") && config.getBoolean("removeAfterShared");
190
+ // removeAfterFailedToShare = config.hasKey("removeAfterFailedToShare") && config.getBoolean("removeAfterFailedToShare");
172
191
  openDocumentsOnFinish = config.hasKey("openDocumentsOnFinish") && config.getBoolean("openDocumentsOnFinish");
173
192
  openShareSheetOnFinish = config.hasKey("openShareSheetOnFinish") && config.getBoolean("openShareSheetOnFinish");
174
193
 
175
194
  isVideoType = !config.hasKey("type") || !Objects.equals(config.getString("type"), "audio");
176
195
 
196
+ closeWhenFinish = !config.hasKey("closeWhenFinish") || config.getBoolean("closeWhenFinish");
177
197
 
178
198
  Activity activity = getReactApplicationContext().getCurrentActivity();
179
199
 
@@ -202,7 +222,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
202
222
  trimmerView.onDestroy();
203
223
  trimmerView = null;
204
224
  }
205
- hideDialog();
225
+ hideDialog(true);
206
226
  sendEvent(getReactApplicationContext(), "onHide", null);
207
227
  });
208
228
  sendEvent(getReactApplicationContext(), "onShow", null);
@@ -230,7 +250,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
230
250
 
231
251
  @Override
232
252
  public void onHostDestroy() {
233
- hideDialog();
253
+ hideDialog(true);
254
+ }
255
+
256
+ @Override
257
+ public void onLoad(int duration) {
258
+ WritableMap map = Arguments.createMap();
259
+ map.putInt("duration", duration);
260
+ sendEvent(getReactApplicationContext(), "onLoad", map);
234
261
  }
235
262
 
236
263
  @Override
@@ -282,16 +309,23 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
282
309
  StorageUtil.deleteFile(in);
283
310
  }
284
311
  } finally {
285
- hideDialog();
312
+ hideDialog(closeWhenFinish);
286
313
  }
287
314
  } else if (openDocumentsOnFinish) {
288
315
  saveFileToExternalStorage(new File(in));
289
316
  } else if (openShareSheetOnFinish) {
290
- hideDialog();
317
+ hideDialog(closeWhenFinish);
291
318
  shareFile(getReactApplicationContext(), new File(in));
319
+ } else {
320
+ hideDialog(closeWhenFinish);
292
321
  }
293
322
  }
294
323
 
324
+ @Override
325
+ public void onCancelTrim() {
326
+ sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
327
+ }
328
+
295
329
  @Override
296
330
  public void onError(String errorMessage, ErrorCode errorCode) {
297
331
  WritableMap map = Arguments.createMap();
@@ -303,8 +337,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
303
337
  @Override
304
338
  public void onCancel() {
305
339
  if (!enableCancelDialog) {
306
- sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
307
- hideDialog();
340
+ sendEvent(getReactApplicationContext(), "onCancel", null);
341
+ hideDialog(true);
308
342
  return;
309
343
  }
310
344
 
@@ -314,8 +348,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
314
348
  builder.setCancelable(false);
315
349
  builder.setPositiveButton(cancelDialogConfirmText, (dialog, which) -> {
316
350
  dialog.cancel();
317
- sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
318
- hideDialog();
351
+ sendEvent(getReactApplicationContext(), "onCancel", null);
352
+ hideDialog(true);
319
353
  });
320
354
  builder.setNegativeButton(cancelDialogCancelText, (dialog, which) -> {
321
355
  dialog.cancel();
@@ -356,18 +390,28 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
356
390
  sendEvent(getReactApplicationContext(), "onStatistics", statistics);
357
391
  }
358
392
 
359
- private void hideDialog() {
393
+ private void hideDialog(boolean shouldCloseEditor) {
394
+ // handle the case when the cancel dialog is still showing but the trimming is finished
395
+ if (cancelTrimmingConfirmDialog != null) {
396
+ if (cancelTrimmingConfirmDialog.isShowing()) {
397
+ cancelTrimmingConfirmDialog.dismiss();
398
+ }
399
+ cancelTrimmingConfirmDialog = null;
400
+ }
401
+
360
402
  if (mProgressDialog != null) {
361
403
  if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
362
404
  mProgressBar = null;
363
405
  mProgressDialog = null;
364
406
  }
365
407
 
366
- if (alertDialog != null) {
367
- if (alertDialog.isShowing()) {
368
- alertDialog.dismiss();
408
+ if (shouldCloseEditor) {
409
+ if (alertDialog != null) {
410
+ if (alertDialog.isShowing()) {
411
+ alertDialog.dismiss();
412
+ }
413
+ alertDialog = null;
369
414
  }
370
- alertDialog = null;
371
415
  }
372
416
  }
373
417
 
@@ -390,6 +434,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
390
434
  ViewGroup.LayoutParams.WRAP_CONTENT
391
435
  ));
392
436
  textView.setText(trimmingText);
437
+ textView.setGravity(Gravity.CENTER);
393
438
  textView.setTextSize(18);
394
439
  layout.addView(textView);
395
440
 
@@ -402,6 +447,49 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
402
447
  mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
403
448
  layout.addView(mProgressBar);
404
449
 
450
+ // Create button
451
+ if (enableCancelTrimming) {
452
+ Button button = new Button(activity);
453
+ button.setLayoutParams(new ViewGroup.LayoutParams(
454
+ ViewGroup.LayoutParams.WRAP_CONTENT,
455
+ ViewGroup.LayoutParams.WRAP_CONTENT
456
+ ));
457
+ // Set the text and style it like a text button
458
+ button.setText(cancelTrimmingButtonText);
459
+ button.setTextColor(ContextCompat.getColor(activity, android.R.color.holo_red_light)); // or use your custom color
460
+
461
+ // Apply ripple effect while keeping the button background transparent
462
+ TypedValue outValue = new TypedValue();
463
+ activity.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
464
+ button.setBackgroundResource(outValue.resourceId);
465
+ button.setOnClickListener(v -> {
466
+ if (enableCancelTrimmingDialog) {
467
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
468
+ builder.setMessage(cancelTrimmingDialogMessage);
469
+ builder.setTitle(cancelTrimmingDialogTitle);
470
+ builder.setCancelable(false);
471
+ builder.setPositiveButton(cancelTrimmingDialogConfirmText, (dialog, which) -> {
472
+ trimmerView.onCancelTrimClicked();
473
+
474
+ if (mProgressDialog != null && mProgressDialog.isShowing()) {
475
+ mProgressDialog.dismiss();
476
+ }
477
+ });
478
+ builder.setNegativeButton(cancelTrimmingDialogCancelText, (dialog, which) -> {
479
+ dialog.cancel();
480
+ });
481
+ cancelTrimmingConfirmDialog = builder.create();
482
+ cancelTrimmingConfirmDialog.show();
483
+ } else {
484
+ trimmerView.onCancelTrimClicked();
485
+ if (mProgressDialog != null && mProgressDialog.isShowing()) {
486
+ mProgressDialog.dismiss();
487
+ }
488
+ }
489
+ });
490
+ layout.addView(button);
491
+ }
492
+
405
493
  // Create the AlertDialog
406
494
  AlertDialog.Builder builder = new AlertDialog.Builder(activity);
407
495
  builder.setCancelable(false);
@@ -461,7 +549,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
461
549
 
462
550
  @ReactMethod
463
551
  private void closeEditor() {
464
- hideDialog();
552
+ hideDialog(true);
465
553
  }
466
554
 
467
555
  @ReactMethod
@@ -4,8 +4,7 @@ public enum ErrorCode {
4
4
  TRIMMING_FAILED,
5
5
  FAIL_TO_GET_VIDEO_INFO,
6
6
  FAIL_TO_INITIALIZE_AUDIO_PLAYER,
7
- FAIL_TO_LOAD_AUDIO,
8
- FAIL_TO_LOAD_VIDEO,
7
+ FAIL_TO_LOAD_MEDIA,
9
8
  FAIL_TO_SAVE_TO_PHOTO,
10
9
  FAIL_TO_SAVE_TO_DOCUMENTS
11
10
  }
@@ -4,9 +4,11 @@ import com.facebook.react.bridge.WritableMap;
4
4
  import com.videotrim.enums.ErrorCode;
5
5
 
6
6
  public interface VideoTrimListener {
7
+ void onLoad(int duration);
7
8
  void onStartTrim();
8
9
  void onTrimmingProgress(int percentage);
9
10
  void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
11
+ void onCancelTrim();
10
12
  void onError(String errorMessage, ErrorCode errorCode);
11
13
  void onCancel();
12
14
  void onSave();
@@ -1,8 +1,10 @@
1
1
  package com.videotrim.utils;
2
2
 
3
+ import android.annotation.SuppressLint;
3
4
  import android.graphics.Bitmap;
4
5
  import android.media.MediaMetadataRetriever;
5
6
  import com.arthenica.ffmpegkit.FFmpegKit;
7
+ import com.arthenica.ffmpegkit.FFmpegSession;
6
8
  import com.arthenica.ffmpegkit.ReturnCode;
7
9
  import com.arthenica.ffmpegkit.SessionState;
8
10
  import com.facebook.react.bridge.Arguments;
@@ -37,12 +39,12 @@ public class VideoTrimmerUtil {
37
39
  public static final int THUMB_HEIGHT = UnitConverter.dpToPx(50); // x2 for better resolution
38
40
  private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
39
41
 
40
- public static void trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, final VideoTrimListener callback) {
42
+ public static FFmpegSession trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, final VideoTrimListener callback) {
41
43
  // Get the current date and time
42
44
  Date currentDate = new Date();
43
45
 
44
46
  // Create a SimpleDateFormat object with the desired format
45
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
47
+ @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
46
48
 
47
49
  // Set the timezone to UTC
48
50
  dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@@ -65,18 +67,19 @@ public class VideoTrimmerUtil {
65
67
  "creation_time=" + formattedDateTime,
66
68
  outputFile
67
69
  };
68
- System.out.println("Commandddddd: " + String.join(",", cmds));
70
+ System.out.println("Command: " + String.join(",", cmds));
69
71
 
70
- FFmpegKit.executeWithArgumentsAsync(cmds, session -> {
72
+ return FFmpegKit.executeWithArgumentsAsync(cmds, session -> {
71
73
  SessionState state = session.getState();
72
74
  ReturnCode returnCode = session.getReturnCode();
73
-
74
- if (ReturnCode.isSuccess(returnCode)) {
75
+ if (ReturnCode.isSuccess(session.getReturnCode())) {
75
76
  // SUCCESS
76
77
  callback.onFinishTrim(outputFile, startMs, endMs, videoDuration);
77
- }
78
- else {
79
- // CANCEL + FAILURE
78
+ } else if (ReturnCode.isCancel(session.getReturnCode())) {
79
+ // CANCEL
80
+ callback.onCancelTrim();
81
+ } else {
82
+ // FAILURE
80
83
  String errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
81
84
  callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED);
82
85
  }