react-native-video-trim 2.1.0 → 2.2.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.
package/README.md CHANGED
@@ -9,6 +9,12 @@
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
+ <img src="images/document_picker.png" width="300" />
17
+ <img src="images/share_sheet.png" width="300" />
12
18
 
13
19
  ## Installation
14
20
 
@@ -76,6 +82,11 @@ export default function App() {
76
82
  const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
77
83
  const subscription = eventEmitter.addListener('VideoTrim', (event) => {
78
84
  switch (event.name) {
85
+ case 'onLoad': {
86
+ // on media loaded successfully
87
+ console.log('onLoadListener', event);
88
+ break;
89
+ }
79
90
  case 'onShow': {
80
91
  console.log('onShowListener', event);
81
92
  break;
@@ -96,10 +107,22 @@ export default function App() {
96
107
  console.log('onCancelTrimming', event);
97
108
  break;
98
109
  }
110
+ case 'onCancel': {
111
+ console.log('onCancel', event);
112
+ break;
113
+ }
99
114
  case 'onError': {
100
115
  console.log('onError', event);
101
116
  break;
102
117
  }
118
+ case 'onLog': {
119
+ console.log('onLog', event);
120
+ break;
121
+ }
122
+ case 'onStatistics': {
123
+ console.log('onStatistics', event);
124
+ break;
125
+ }
103
126
  }
104
127
  });
105
128
 
@@ -161,41 +184,83 @@ Main method to show Video Editor UI.
161
184
 
162
185
  *Params*:
163
186
  - `videoPath`: Path to video file, if this is an invalid path, `onError` event will be fired
164
- - `config` (optional):
187
+ - `config` (optional, every sub props of `config` is optional):
165
188
 
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
189
+ - `type` (`default = video`): which player to use, `video` or `audio`
190
+ - `outputExt` (`default = mp4`): output file extension
191
+ - `enableHapticFeedback` (`default = true`): whether to enable haptic feedback
192
+ - `saveToPhoto` (Video-only, `default = false`): whether to save video to photo/gallery after editing
193
+ - `openDocumentsOnFinish` (`default = false`): open Document Picker on done trimming
194
+ - `openShareSheetOnFinish` (`default = false`): open Share Sheet on done trimming
195
+ - `removeAfterSavedToPhoto` (`default = false`): whether to remove output file from storage after saved to Photo successfully
196
+ - `removeAfterFailedToSavePhoto` (`default = false`): whether to remove output file if fail to save to Photo
197
+ - `removeAfterSavedToDocuments` (`default = false`): whether to remove output file from storage after saved Documents successfully
198
+ - `removeAfterFailedToSaveDocuments` (`default = false`): whether to remove output file from storage after fail to save to Documents
199
+ - `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)
200
+ - `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
201
  - `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
202
+ - `minDuration` (`default = 1000`): minimum duration for the trimmed video
203
+ - `cancelButtonText` (`default= "Cancel"`): text of left button in Editor dialog
204
+ - `saveButtonText` (`default= "Save"`): text of right button in Editor dialog
205
+ - `enableCancelDialog` (`default = true`): whether to show alert dialog on press Cancel
206
+ - `cancelDialogTitle` (`default = "Warning!"`)
207
+ - `cancelDialogMessage` (`default = "Are you sure want to cancel?"`)
208
+ - `cancelDialogCancelText` (`default = "Close"`)
209
+ - `cancelDialogConfirmText` (`default = "Proceed"`)
210
+ - `enableSaveDialog` (`default = true`): whether to show alert dialog on press Save
211
+ - `saveDialogTitle` (`default = "Confirmation!"`)
212
+ - `saveDialogMessage` (`default = "Are you sure want to save?"`)
213
+ - `saveDialogCancelText` (`default = "Close"`)
214
+ - `saveDialogConfirmText` (`default = "Proceed"`)
215
+ - `fullScreenModalIOS` (`default = false`): whether to open editor in fullscreen modal
216
+ - `trimmingText` (`default = "Trimming video..."`): trimming text on the progress dialog
217
+ - `autoplay` (`default = false`): whether to autoplay media on load
218
+ - `jumpToPositionOnLoad` (optional): which time position should jump on media loaded (millisecond)
219
+ - `closeWhenFinish` (`default = true`): should editor close on finish trimming
220
+ - `enableCancelTrimming` (`default = true`): enable cancel trimming
221
+ - `cancelTrimmingButtonText` (`default = "Cancel"`)
222
+ - `enableCancelTrimmingDialog` (`default = true`)
223
+ - `cancelTrimmingDialogTitle` (`default = "Warning!"`)
224
+ - `cancelTrimmingDialogMessage` (`default = "Are you sure want to cancel trimming?"`)
225
+ - `cancelTrimmingDialogCancelText` (`default = "Close"`)
226
+ - `cancelTrimmingDialogConfirmText` (`default = "Proceed"`)
227
+ - `headerText` (optional)
228
+ - `headerTextSize` (`default = 16`)
229
+ - `headerTextColor` (`default = white`)
230
+ - `alertOnFailToLoad` (`default = true`)
231
+ - `alertOnFailTitle` (`default = "Error"`)
232
+ - `alertOnFailMessage` (`default = "Fail to load media. Possibly invalid file or no network connection"`)
233
+ - `alertOnFailCloseText` (`default = "Close"`)
194
234
 
195
235
  If `saveToPhoto = true`, you must ensure that you have request permission to write to photo/gallery
196
236
  - For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
197
237
  - For iOS: you need `NSPhotoLibraryUsageDescription` in Info.plist
198
238
 
239
+ If `openShareSheetOnFinish=true`, on Android you'll need to update `AndroidManifest.xml` like below:
240
+ ```xml
241
+ </application>
242
+ ...
243
+ <provider
244
+ android:name="androidx.core.content.FileProvider"
245
+ android:authorities="${applicationId}.provider"
246
+ android:exported="false"
247
+ android:grantUriPermissions="true">
248
+ <meta-data
249
+ android:name="android.support.FILE_PROVIDER_PATHS"
250
+ android:resource="@xml/file_paths" />
251
+ </provider>
252
+ </application>
253
+ ```
254
+
255
+ 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:
256
+ ```xml
257
+ <?xml version="1.0" encoding="utf-8"?>
258
+ <paths xmlns:android="http://schemas.android.com/apk/res/android">
259
+ <files-path name="internal_files" path="." />
260
+ <external-path name="external_files" path="." />
261
+ </paths>
262
+ ```
263
+
199
264
  ## isValidFile(videoPath: string)
200
265
 
201
266
  This method is to check if a path is a valid video/audio
@@ -220,43 +285,43 @@ useEffect(() => {
220
285
  const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
221
286
  const subscription = eventEmitter.addListener('VideoTrim', (event) => {
222
287
  switch (event.name) {
288
+ case 'onLoad': {
289
+ console.log('onLoadListener', event);
290
+ break;
291
+ }
223
292
  case 'onShow': {
224
- // on Dialog show
225
293
  console.log('onShowListener', event);
226
294
  break;
227
295
  }
228
296
  case 'onHide': {
229
- // on Dialog hide
230
297
  console.log('onHide', event);
231
298
  break;
232
299
  }
233
300
  case 'onStartTrimming': {
234
- // on start trimming
235
301
  console.log('onStartTrimming', event);
236
302
  break;
237
303
  }
238
304
  case 'onFinishTrimming': {
239
- // on trimming is done
240
305
  console.log('onFinishTrimming', event);
241
306
  break;
242
307
  }
243
308
  case 'onCancelTrimming': {
244
- // when user clicks Cancel button
245
309
  console.log('onCancelTrimming', event);
246
310
  break;
247
311
  }
312
+ case 'onCancel': {
313
+ console.log('onCancel', event);
314
+ break;
315
+ }
248
316
  case 'onError': {
249
- // any error occured: invalid file, lack of permissions to write to photo/gallery, unexpected error...
250
317
  console.log('onError', event);
251
318
  break;
252
319
  }
253
320
  case 'onLog': {
254
- // FFMPEG logs (while trimming)
255
321
  console.log('onLog', event);
256
322
  break;
257
323
  }
258
324
  case 'onStatistics': {
259
- // FFMPEG stats (while trimming)
260
325
  console.log('onStatistics', event);
261
326
  break;
262
327
  }
@@ -268,6 +333,52 @@ useEffect(() => {
268
333
  };
269
334
  }, []);
270
335
  ```
336
+ # Audio support
337
+ <img src="images/audio_android.jpg" width="200" />
338
+ <img src="images/audio_ios.jpg" width="200" />
339
+
340
+ For audio only you have to pass `type=audio` and `outputExt`:
341
+ ```ts
342
+ showEditor(url, {
343
+ type: 'audio', // important
344
+ outputExt: 'wav', // important: any audio type for output file extension
345
+ })
346
+ ```
347
+
348
+ You must install FFMPEG version from "https" onwards, "min" version won't work. Eg.
349
+ ```gradle
350
+ // Android: android/build.gradle > buildscript > ext
351
+
352
+ buildscript {
353
+ ext {
354
+ ffmpegKitPackage = "full"
355
+ }
356
+
357
+ ---
358
+ // iOS:
359
+
360
+ FFMPEGKIT_PACKAGE=https npx pod-install ios
361
+
362
+ // or
363
+
364
+ FFMPEGKIT_PACKAGE=https pod install
365
+ ```
366
+
367
+ # Cancel trimming
368
+ <img src="images/progress.jpg" width="200" />
369
+ <img src="images/cancel_confirm.jpg" width="200" />
370
+
371
+ While trimming, you can press Cancel to terminate the process.
372
+
373
+ Related props: `enableCancelTrimming, cancelTrimmingButtonText, enableCancelTrimmingDialog, cancelTrimmingDialogTitle, cancelTrimmingDialogMessage, cancelTrimmingDialogCancelText, cancelTrimmingDialogConfirmText`
374
+
375
+ # Fail to load media
376
+ <img src="images/fail_to_load_media.jpg" width="200" />
377
+
378
+ If there's error while loading media, there'll be a prompt
379
+
380
+ Related props: `alertOnFailToLoad, alertOnFailTitle, alertOnFailMessage, alertOnFailCloseText`
381
+
271
382
  # FFMPEG Version
272
383
  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
384
 
@@ -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,21 @@ 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));
292
319
  }
293
320
  }
294
321
 
322
+ @Override
323
+ public void onCancelTrim() {
324
+ sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
325
+ }
326
+
295
327
  @Override
296
328
  public void onError(String errorMessage, ErrorCode errorCode) {
297
329
  WritableMap map = Arguments.createMap();
@@ -303,8 +335,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
303
335
  @Override
304
336
  public void onCancel() {
305
337
  if (!enableCancelDialog) {
306
- sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
307
- hideDialog();
338
+ sendEvent(getReactApplicationContext(), "onCancel", null);
339
+ hideDialog(true);
308
340
  return;
309
341
  }
310
342
 
@@ -314,8 +346,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
314
346
  builder.setCancelable(false);
315
347
  builder.setPositiveButton(cancelDialogConfirmText, (dialog, which) -> {
316
348
  dialog.cancel();
317
- sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
318
- hideDialog();
349
+ sendEvent(getReactApplicationContext(), "onCancel", null);
350
+ hideDialog(true);
319
351
  });
320
352
  builder.setNegativeButton(cancelDialogCancelText, (dialog, which) -> {
321
353
  dialog.cancel();
@@ -356,18 +388,28 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
356
388
  sendEvent(getReactApplicationContext(), "onStatistics", statistics);
357
389
  }
358
390
 
359
- private void hideDialog() {
391
+ private void hideDialog(boolean shouldCloseEditor) {
392
+ // handle the case when the cancel dialog is still showing but the trimming is finished
393
+ if (cancelTrimmingConfirmDialog != null) {
394
+ if (cancelTrimmingConfirmDialog.isShowing()) {
395
+ cancelTrimmingConfirmDialog.dismiss();
396
+ }
397
+ cancelTrimmingConfirmDialog = null;
398
+ }
399
+
360
400
  if (mProgressDialog != null) {
361
401
  if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
362
402
  mProgressBar = null;
363
403
  mProgressDialog = null;
364
404
  }
365
405
 
366
- if (alertDialog != null) {
367
- if (alertDialog.isShowing()) {
368
- alertDialog.dismiss();
406
+ if (shouldCloseEditor) {
407
+ if (alertDialog != null) {
408
+ if (alertDialog.isShowing()) {
409
+ alertDialog.dismiss();
410
+ }
411
+ alertDialog = null;
369
412
  }
370
- alertDialog = null;
371
413
  }
372
414
  }
373
415
 
@@ -390,6 +432,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
390
432
  ViewGroup.LayoutParams.WRAP_CONTENT
391
433
  ));
392
434
  textView.setText(trimmingText);
435
+ textView.setGravity(Gravity.CENTER);
393
436
  textView.setTextSize(18);
394
437
  layout.addView(textView);
395
438
 
@@ -402,6 +445,49 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
402
445
  mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
403
446
  layout.addView(mProgressBar);
404
447
 
448
+ // Create button
449
+ if (enableCancelTrimming) {
450
+ Button button = new Button(activity);
451
+ button.setLayoutParams(new ViewGroup.LayoutParams(
452
+ ViewGroup.LayoutParams.WRAP_CONTENT,
453
+ ViewGroup.LayoutParams.WRAP_CONTENT
454
+ ));
455
+ // Set the text and style it like a text button
456
+ button.setText(cancelTrimmingButtonText);
457
+ button.setTextColor(ContextCompat.getColor(activity, android.R.color.holo_red_light)); // or use your custom color
458
+
459
+ // Apply ripple effect while keeping the button background transparent
460
+ TypedValue outValue = new TypedValue();
461
+ activity.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
462
+ button.setBackgroundResource(outValue.resourceId);
463
+ button.setOnClickListener(v -> {
464
+ if (enableCancelTrimmingDialog) {
465
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
466
+ builder.setMessage(cancelTrimmingDialogMessage);
467
+ builder.setTitle(cancelTrimmingDialogTitle);
468
+ builder.setCancelable(false);
469
+ builder.setPositiveButton(cancelTrimmingDialogConfirmText, (dialog, which) -> {
470
+ trimmerView.onCancelTrimClicked();
471
+
472
+ if (mProgressDialog != null && mProgressDialog.isShowing()) {
473
+ mProgressDialog.dismiss();
474
+ }
475
+ });
476
+ builder.setNegativeButton(cancelTrimmingDialogCancelText, (dialog, which) -> {
477
+ dialog.cancel();
478
+ });
479
+ cancelTrimmingConfirmDialog = builder.create();
480
+ cancelTrimmingConfirmDialog.show();
481
+ } else {
482
+ trimmerView.onCancelTrimClicked();
483
+ if (mProgressDialog != null && mProgressDialog.isShowing()) {
484
+ mProgressDialog.dismiss();
485
+ }
486
+ }
487
+ });
488
+ layout.addView(button);
489
+ }
490
+
405
491
  // Create the AlertDialog
406
492
  AlertDialog.Builder builder = new AlertDialog.Builder(activity);
407
493
  builder.setCancelable(false);
@@ -461,7 +547,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
461
547
 
462
548
  @ReactMethod
463
549
  private void closeEditor() {
464
- hideDialog();
550
+ hideDialog(true);
465
551
  }
466
552
 
467
553
  @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
  }