react-native-video-trim 2.0.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.
Files changed (31) hide show
  1. package/README.md +168 -33
  2. package/android/src/main/AndroidManifest.xml +13 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +282 -75
  4. package/android/src/main/java/com/videotrim/enums/ErrorCode.java +10 -0
  5. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +4 -1
  6. package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
  7. package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
  8. package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +26 -16
  9. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +310 -81
  10. package/android/src/main/res/drawable/airpodsmax.xml +19 -0
  11. package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
  12. package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
  13. package/android/src/main/res/layout/video_trimmer_view.xml +71 -4
  14. package/android/src/main/res/xml/file_paths.xml +5 -0
  15. package/ios/AssetLoader.swift +99 -0
  16. package/ios/ErrorCode.swift +16 -0
  17. package/ios/ProgressAlertController.swift +100 -0
  18. package/ios/VideoTrim.mm +4 -2
  19. package/ios/VideoTrim.swift +472 -177
  20. package/ios/VideoTrimmer.swift +16 -10
  21. package/ios/VideoTrimmerViewController.swift +191 -22
  22. package/lib/commonjs/index.js +25 -55
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/module/index.js +24 -55
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/typescript/index.d.ts +215 -9
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/index.tsx +229 -66
  30. package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
  31. package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
@@ -1,15 +1,21 @@
1
1
  package com.videotrim;
2
2
 
3
+ import static android.app.Activity.RESULT_OK;
3
4
  import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
4
5
 
5
6
  import android.app.Activity;
7
+ import android.content.Context;
8
+ import android.content.pm.PackageManager;
9
+ import android.content.pm.ResolveInfo;
6
10
  import android.content.res.ColorStateList;
7
11
  import android.graphics.Color;
8
- import android.media.MediaMetadataRetriever;
9
12
  import android.net.Uri;
10
13
  import android.os.Build;
14
+ import android.util.Log;
15
+ import android.util.TypedValue;
11
16
  import android.view.Gravity;
12
17
  import android.view.ViewGroup;
18
+ import android.widget.Button;
13
19
  import android.widget.LinearLayout;
14
20
  import android.widget.ProgressBar;
15
21
  import android.widget.TextView;
@@ -17,8 +23,12 @@ import android.widget.TextView;
17
23
  import androidx.annotation.NonNull;
18
24
  import androidx.annotation.Nullable;
19
25
  import androidx.appcompat.app.AlertDialog;
26
+ import androidx.core.content.ContextCompat;
27
+ import androidx.core.content.FileProvider;
20
28
 
29
+ import com.facebook.react.bridge.ActivityEventListener;
21
30
  import com.facebook.react.bridge.Arguments;
31
+ import com.facebook.react.bridge.BaseActivityEventListener;
22
32
  import com.facebook.react.bridge.LifecycleEventListener;
23
33
  import com.facebook.react.bridge.Promise;
24
34
  import com.facebook.react.bridge.ReactApplicationContext;
@@ -29,25 +39,43 @@ import com.facebook.react.bridge.ReadableMap;
29
39
  import com.facebook.react.bridge.WritableMap;
30
40
  import com.facebook.react.module.annotations.ReactModule;
31
41
  import com.facebook.react.modules.core.DeviceEventManagerModule;
42
+ import com.videotrim.enums.ErrorCode;
32
43
  import com.videotrim.interfaces.VideoTrimListener;
44
+ import com.videotrim.utils.MediaMetadataUtil;
33
45
  import com.videotrim.utils.StorageUtil;
46
+ import com.videotrim.utils.VideoTrimmerUtil;
34
47
  import com.videotrim.widgets.VideoTrimmerView;
35
48
 
49
+ import android.content.Intent;
50
+
51
+ import java.io.File;
52
+ import java.io.FileInputStream;
36
53
  import java.io.IOException;
54
+ import java.io.OutputStream;
55
+ import java.util.Objects;
56
+
37
57
  import iknow.android.utils.BaseUtils;
38
58
 
39
59
  @ReactModule(name = VideoTrimModule.NAME)
40
60
  public class VideoTrimModule extends ReactContextBaseJavaModule implements VideoTrimListener, LifecycleEventListener {
61
+ private static final String TAG = VideoTrimmerUtil.class.getSimpleName();
62
+
41
63
  public static final String NAME = "VideoTrim";
42
64
  private static Boolean isInit = false;
43
65
  private VideoTrimmerView trimmerView;
44
66
  private AlertDialog alertDialog;
45
67
  private AlertDialog mProgressDialog;
68
+ private AlertDialog cancelTrimmingConfirmDialog;
46
69
  private ProgressBar mProgressBar;
47
70
  private int listenerCount = 0;
48
-
49
- private Promise showEditorPromise;
50
-
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";
51
79
  private boolean enableCancelDialog = true;
52
80
  private String cancelDialogTitle = "Warning!";
53
81
  private String cancelDialogMessage = "Are you sure want to cancel?";
@@ -57,11 +85,66 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
57
85
  private String saveDialogTitle = "Confirmation!";
58
86
  private String saveDialogMessage = "Are you sure want to save?";
59
87
  private String saveDialogCancelText = "Close";
60
- private String saveDialogConfirmText = "Proceed";
88
+ private String saveDialogConfirmText = "Proceed";
61
89
  private String trimmingText = "Trimming video...";
90
+ private String outputFile;
91
+ private boolean saveToPhoto = false;
92
+ private boolean removeAfterSavedToPhoto = false;
93
+ private boolean removeAfterFailedToSavePhoto = false;
94
+ private boolean removeAfterSavedToDocuments = false;
95
+ private boolean removeAfterFailedToSaveDocuments = false;
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
98
+ private boolean openDocumentsOnFinish = false;
99
+ private boolean openShareSheetOnFinish = false;
100
+ private boolean isVideoType = true;
101
+ private boolean closeWhenFinish = true;
102
+
103
+ private static final int REQUEST_CODE_SAVE_FILE = 1;
62
104
 
63
105
  public VideoTrimModule(ReactApplicationContext reactContext) {
64
106
  super(reactContext);
107
+ ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
108
+
109
+ @Override
110
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
111
+ if (requestCode == REQUEST_CODE_SAVE_FILE && resultCode == RESULT_OK) {
112
+ Uri uri = intent.getData();
113
+ if (uri == null) {
114
+ return;
115
+ }
116
+ try {
117
+ OutputStream outputStream = reactContext.getContentResolver().openOutputStream(uri);
118
+ if (outputStream == null) {
119
+ return;
120
+ }
121
+ FileInputStream fileInputStream = new FileInputStream(outputFile);
122
+ byte[] buffer = new byte[1024];
123
+ int length;
124
+ while ((length = fileInputStream.read(buffer)) > 0) {
125
+ outputStream.write(buffer, 0, length);
126
+ }
127
+ outputStream.close();
128
+ fileInputStream.close();
129
+ // File saved successfully
130
+ Log.d(TAG, "File saved successfully to " + uri);
131
+ if (removeAfterSavedToDocuments) {
132
+ StorageUtil.deleteFile(outputFile);
133
+ }
134
+ } catch (Exception e) {
135
+ e.printStackTrace();
136
+ // Handle the error
137
+ onError("Failed to save edited video to Documents: " + e.getLocalizedMessage(), ErrorCode.FAIL_TO_SAVE_TO_DOCUMENTS);
138
+ if (removeAfterFailedToSaveDocuments) {
139
+ StorageUtil.deleteFile(outputFile);
140
+ }
141
+ } finally {
142
+ hideDialog(true);
143
+ }
144
+ }
145
+ }
146
+ };
147
+ reactContext.addActivityEventListener(mActivityEventListener);
65
148
  }
66
149
 
67
150
  @Override
@@ -72,32 +155,46 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
72
155
 
73
156
 
74
157
  @ReactMethod
75
- public void showEditor(String videoPath, ReadableMap config, Promise promise) {
76
- showEditorPromise = promise;
158
+ public void showEditor(String videoPath, ReadableMap config) {
77
159
  if (trimmerView != null || alertDialog != null) {
78
160
  return;
79
161
  }
162
+ enableCancelTrimming = !config.hasKey("enableCancelTrimming") || config.getBoolean("enableCancelTrimming");
80
163
 
81
- if (!isValidVideo(videoPath)) {
82
- WritableMap map = Arguments.createMap();
83
- map.putString("message", "File is not a valid video");
84
- sendEvent(getReactApplicationContext(), "onError", map);
85
- return;
86
- }
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";
87
170
 
88
- enableCancelDialog = config.hasKey("enableCancelDialog") ? config.getBoolean("enableCancelDialog") : true;
171
+ enableCancelDialog = !config.hasKey("enableCancelDialog") || config.getBoolean("enableCancelDialog");
89
172
  cancelDialogTitle = config.hasKey("cancelDialogTitle") ? config.getString("cancelDialogTitle") : "Warning!";
90
173
  cancelDialogMessage = config.hasKey("cancelDialogMessage") ? config.getString("cancelDialogMessage") : "Are you sure want to cancel?";
91
174
  cancelDialogCancelText = config.hasKey("cancelDialogCancelText") ? config.getString("cancelDialogCancelText") : "Close";
92
175
  cancelDialogConfirmText = config.hasKey("cancelDialogConfirmText") ? config.getString("cancelDialogConfirmText") : "Proceed";
93
176
 
94
- enableSaveDialog = config.hasKey("enableSaveDialog") ? config.getBoolean("enableSaveDialog") : true;
177
+ enableSaveDialog = !config.hasKey("enableSaveDialog") || config.getBoolean("enableSaveDialog");
95
178
  saveDialogTitle = config.hasKey("saveDialogTitle") ? config.getString("saveDialogTitle") : "Confirmation!";
96
179
  saveDialogMessage = config.hasKey("saveDialogMessage") ? config.getString("saveDialogMessage") : "Are you sure want to save?";
97
180
  saveDialogCancelText = config.hasKey("saveDialogCancelText") ? config.getString("saveDialogCancelText") : "Close";
98
181
  saveDialogConfirmText = config.hasKey("saveDialogConfirmText") ? config.getString("saveDialogConfirmText") : "Proceed";
99
182
  trimmingText = config.hasKey("trimmingText") ? config.getString("trimmingText") : "Trimming video...";
100
183
 
184
+ saveToPhoto = config.hasKey("saveToPhoto") && config.getBoolean("saveToPhoto");
185
+ removeAfterSavedToPhoto = config.hasKey("removeAfterSavedToPhoto") && config.getBoolean("removeAfterSavedToPhoto");
186
+ removeAfterFailedToSavePhoto = config.hasKey("removeAfterFailedToSavePhoto") && config.getBoolean("removeAfterFailedToSavePhoto");
187
+ removeAfterSavedToDocuments = config.hasKey("removeAfterSavedToDocuments") && config.getBoolean("removeAfterSavedToDocuments");
188
+ removeAfterFailedToSaveDocuments = config.hasKey("removeAfterFailedToSaveDocuments") && config.getBoolean("removeAfterFailedToSaveDocuments");
189
+ // removeAfterShared = config.hasKey("removeAfterShared") && config.getBoolean("removeAfterShared");
190
+ // removeAfterFailedToShare = config.hasKey("removeAfterFailedToShare") && config.getBoolean("removeAfterFailedToShare");
191
+ openDocumentsOnFinish = config.hasKey("openDocumentsOnFinish") && config.getBoolean("openDocumentsOnFinish");
192
+ openShareSheetOnFinish = config.hasKey("openShareSheetOnFinish") && config.getBoolean("openShareSheetOnFinish");
193
+
194
+ isVideoType = !config.hasKey("type") || !Objects.equals(config.getString("type"), "audio");
195
+
196
+ closeWhenFinish = !config.hasKey("closeWhenFinish") || config.getBoolean("closeWhenFinish");
197
+
101
198
  Activity activity = getReactApplicationContext().getCurrentActivity();
102
199
 
103
200
  if (!isInit) {
@@ -110,7 +207,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
110
207
  runOnUiThread(() -> {
111
208
  trimmerView = new VideoTrimmerView(getReactApplicationContext(), config, null);
112
209
  trimmerView.setOnTrimVideoListener(this);
113
- trimmerView.initVideoByURI(Uri.parse(videoPath));
210
+ trimmerView.initByURI(Uri.parse(videoPath));
114
211
 
115
212
  AlertDialog.Builder builder = new AlertDialog.Builder(activity, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
116
213
  builder.setCancelable(false);
@@ -125,7 +222,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
125
222
  trimmerView.onDestroy();
126
223
  trimmerView = null;
127
224
  }
128
- hideDialog();
225
+ hideDialog(true);
129
226
  sendEvent(getReactApplicationContext(), "onHide", null);
130
227
  });
131
228
  sendEvent(getReactApplicationContext(), "onShow", null);
@@ -140,29 +237,39 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
140
237
 
141
238
  @Override
142
239
  public void onHostResume() {
143
-
240
+ Log.d(TAG, "onHostResume: ");
144
241
  }
145
242
 
146
243
  @Override
147
244
  public void onHostPause() {
245
+ Log.d(TAG, "onHostPause: ");
148
246
  if (trimmerView != null) {
149
- trimmerView.onVideoPause();
247
+ trimmerView.onMediaPause();
150
248
  }
151
249
  }
152
250
 
153
251
  @Override
154
252
  public void onHostDestroy() {
155
- hideDialog();
253
+ hideDialog(true);
156
254
  }
157
255
 
158
- @Override public void onStartTrim() {
256
+ @Override
257
+ public void onLoad(int duration) {
258
+ WritableMap map = Arguments.createMap();
259
+ map.putInt("duration", duration);
260
+ sendEvent(getReactApplicationContext(), "onLoad", map);
261
+ }
262
+
263
+ @Override
264
+ public void onStartTrim() {
159
265
  sendEvent(getReactApplicationContext(), "onStartTrimming", null);
160
266
  runOnUiThread(() -> {
161
267
  buildDialog();
162
268
  });
163
269
  }
164
270
 
165
- @Override public void onTrimmingProgress(int percentage) {
271
+ @Override
272
+ public void onTrimmingProgress(int percentage) {
166
273
  // prevent onTrimmingProgress is called after onFinishTrim (some rare cases)
167
274
  if (mProgressBar == null) {
168
275
  return;
@@ -176,7 +283,9 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
176
283
  }
177
284
 
178
285
 
179
- @Override public void onFinishTrim(String in, long startTime, long endTime, int duration) {
286
+ @Override
287
+ public void onFinishTrim(String in, long startTime, long endTime, int duration) {
288
+ outputFile = in;
180
289
  runOnUiThread(() -> {
181
290
  WritableMap map = Arguments.createMap();
182
291
  map.putString("outputPath", in);
@@ -184,21 +293,50 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
184
293
  map.putDouble("startTime", (double) startTime);
185
294
  map.putDouble("endTime", (double) endTime);
186
295
  sendEvent(getReactApplicationContext(), "onFinishTrimming", map);
187
- showEditorPromise.resolve(in);
188
296
  });
297
+
298
+ if (saveToPhoto && isVideoType) {
299
+ try {
300
+ StorageUtil.saveVideoToGallery(getReactApplicationContext(), in);
301
+ Log.d(TAG, "Edited video saved to Photo Library successfully.");
302
+ if (removeAfterSavedToPhoto) {
303
+ StorageUtil.deleteFile(in);
304
+ }
305
+ } catch (IOException e) {
306
+ e.printStackTrace();
307
+ onError("Failed to save edited video to Photo Library: " + e.getLocalizedMessage(), ErrorCode.FAIL_TO_SAVE_TO_PHOTO);
308
+ if (removeAfterFailedToSavePhoto) {
309
+ StorageUtil.deleteFile(in);
310
+ }
311
+ } finally {
312
+ hideDialog(closeWhenFinish);
313
+ }
314
+ } else if (openDocumentsOnFinish) {
315
+ saveFileToExternalStorage(new File(in));
316
+ } else if (openShareSheetOnFinish) {
317
+ hideDialog(closeWhenFinish);
318
+ shareFile(getReactApplicationContext(), new File(in));
319
+ }
320
+ }
321
+
322
+ @Override
323
+ public void onCancelTrim() {
324
+ sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
189
325
  }
190
326
 
191
- @Override public void onError(String errorMessage) {
327
+ @Override
328
+ public void onError(String errorMessage, ErrorCode errorCode) {
192
329
  WritableMap map = Arguments.createMap();
193
330
  map.putString("message", errorMessage);
331
+ map.putString("errorCode", errorCode.name());
194
332
  sendEvent(getReactApplicationContext(), "onError", map);
195
- this.hideDialog();
196
333
  }
197
334
 
198
- @Override public void onCancel() {
335
+ @Override
336
+ public void onCancel() {
199
337
  if (!enableCancelDialog) {
200
- sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
201
- hideDialog();
338
+ sendEvent(getReactApplicationContext(), "onCancel", null);
339
+ hideDialog(true);
202
340
  return;
203
341
  }
204
342
 
@@ -208,8 +346,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
208
346
  builder.setCancelable(false);
209
347
  builder.setPositiveButton(cancelDialogConfirmText, (dialog, which) -> {
210
348
  dialog.cancel();
211
- sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
212
- hideDialog();
349
+ sendEvent(getReactApplicationContext(), "onCancel", null);
350
+ hideDialog(true);
213
351
  });
214
352
  builder.setNegativeButton(cancelDialogCancelText, (dialog, which) -> {
215
353
  dialog.cancel();
@@ -218,7 +356,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
218
356
  alertDialog.show();
219
357
  }
220
358
 
221
- @Override public void onSave() {
359
+ @Override
360
+ public void onSave() {
222
361
  if (!enableSaveDialog) {
223
362
  trimmerView.onSaveClicked();
224
363
  return;
@@ -239,27 +378,38 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
239
378
  alertDialog.show();
240
379
  }
241
380
 
242
- @Override public void onLog(WritableMap log) {
381
+ @Override
382
+ public void onLog(WritableMap log) {
243
383
  sendEvent(getReactApplicationContext(), "onLog", log);
244
384
  }
245
385
 
246
- @Override public void onStatistics(WritableMap statistics) {
386
+ @Override
387
+ public void onStatistics(WritableMap statistics) {
247
388
  sendEvent(getReactApplicationContext(), "onStatistics", statistics);
248
389
  }
249
390
 
250
- @ReactMethod
251
- 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
+
252
400
  if (mProgressDialog != null) {
253
401
  if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
254
402
  mProgressBar = null;
255
403
  mProgressDialog = null;
256
404
  }
257
405
 
258
- if (alertDialog != null) {
259
- if(alertDialog.isShowing()) {
260
- alertDialog.dismiss();
406
+ if (shouldCloseEditor) {
407
+ if (alertDialog != null) {
408
+ if (alertDialog.isShowing()) {
409
+ alertDialog.dismiss();
410
+ }
411
+ alertDialog = null;
261
412
  }
262
- alertDialog = null;
263
413
  }
264
414
  }
265
415
 
@@ -282,6 +432,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
282
432
  ViewGroup.LayoutParams.WRAP_CONTENT
283
433
  ));
284
434
  textView.setText(trimmingText);
435
+ textView.setGravity(Gravity.CENTER);
285
436
  textView.setTextSize(18);
286
437
  layout.addView(textView);
287
438
 
@@ -294,6 +445,49 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
294
445
  mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
295
446
  layout.addView(mProgressBar);
296
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
+
297
491
  // Create the AlertDialog
298
492
  AlertDialog.Builder builder = new AlertDialog.Builder(activity);
299
493
  builder.setCancelable(false);
@@ -326,41 +520,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
326
520
  }
327
521
  }
328
522
 
329
-
330
- public boolean isValidVideo(String filePath) {
331
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
332
-
333
- try {
334
- retriever.setDataSource(getReactApplicationContext(), Uri.parse(filePath));
335
- } catch (Exception e){
336
- e.printStackTrace();
337
- return false;
338
- }
339
-
340
- String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
341
- return "yes".equals(hasVideo);
342
- }
343
-
344
- @ReactMethod
345
- private void isValidVideo(String filePath, Promise promise) {
346
- promise.resolve(isValidVideo(filePath));
347
- }
348
-
349
- @ReactMethod
350
- private void saveVideo(String filePath, Promise promise) {
351
- try {
352
- StorageUtil.saveVideoToGallery(getReactApplicationContext(), filePath);
353
- } catch (IOException e) {
354
- e.printStackTrace();
355
- WritableMap mapE = Arguments.createMap();
356
- mapE.putString("message", "Fail while copying file to Gallery");
357
- sendEvent(getReactApplicationContext(), "onError", mapE);
358
- }
359
-
360
- this.hideDialog();
361
- promise.resolve(null);
362
- }
363
-
364
523
  @ReactMethod
365
524
  private void listFiles(Promise promise) {
366
525
  String[] files = StorageUtil.listFiles(getReactApplicationContext());
@@ -385,4 +544,52 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
385
544
  boolean state = StorageUtil.deleteFile(filePath);
386
545
  promise.resolve(state);
387
546
  }
547
+
548
+ @ReactMethod
549
+ private void closeEditor() {
550
+ hideDialog(true);
551
+ }
552
+
553
+ @ReactMethod
554
+ private void isValidFile(String filePath, Promise promise) {
555
+ MediaMetadataUtil.checkFileValidity(filePath, (isValid, fileType, duration) -> {
556
+ if (isValid) {
557
+ System.out.println("Valid " + fileType + " file with duration: " + duration + " milliseconds");
558
+ } else {
559
+ System.out.println("Invalid file");
560
+ }
561
+
562
+ WritableMap map = Arguments.createMap();
563
+ map.putBoolean("isValid", isValid);
564
+ map.putString("fileType", fileType);
565
+ map.putDouble("duration", duration);
566
+ promise.resolve(map);
567
+ });
568
+ }
569
+
570
+ private void saveFileToExternalStorage(File file) {
571
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
572
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
573
+ intent.setType("*/*"); // Change MIME type as needed
574
+ intent.putExtra(Intent.EXTRA_TITLE, file.getName());
575
+ getReactApplicationContext().getCurrentActivity().startActivityForResult(intent, REQUEST_CODE_SAVE_FILE);
576
+ }
577
+
578
+ public void shareFile(Context context, File file) {
579
+ Uri fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
580
+
581
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
582
+ shareIntent.setType("*/*");
583
+ shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
584
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
585
+
586
+ // Grant permissions to all applications that can handle the intent
587
+ for (ResolveInfo resolveInfo : context.getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY)) {
588
+ String packageName = resolveInfo.activityInfo.packageName;
589
+ context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
590
+ }
591
+
592
+ // directly use context.startActivity(shareIntent) will cause crash
593
+ getReactApplicationContext().getCurrentActivity().startActivity(Intent.createChooser(shareIntent, "Share file"));
594
+ }
388
595
  }
@@ -0,0 +1,10 @@
1
+ package com.videotrim.enums;
2
+
3
+ public enum ErrorCode {
4
+ TRIMMING_FAILED,
5
+ FAIL_TO_GET_VIDEO_INFO,
6
+ FAIL_TO_INITIALIZE_AUDIO_PLAYER,
7
+ FAIL_TO_LOAD_MEDIA,
8
+ FAIL_TO_SAVE_TO_PHOTO,
9
+ FAIL_TO_SAVE_TO_DOCUMENTS
10
+ }
@@ -1,12 +1,15 @@
1
1
  package com.videotrim.interfaces;
2
2
 
3
3
  import com.facebook.react.bridge.WritableMap;
4
+ import com.videotrim.enums.ErrorCode;
4
5
 
5
6
  public interface VideoTrimListener {
7
+ void onLoad(int duration);
6
8
  void onStartTrim();
7
9
  void onTrimmingProgress(int percentage);
8
10
  void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
9
- void onError(String errorMessage);
11
+ void onCancelTrim();
12
+ void onError(String errorMessage, ErrorCode errorCode);
10
13
  void onCancel();
11
14
  void onSave();
12
15
  void onLog(WritableMap log);
@@ -0,0 +1,75 @@
1
+ package com.videotrim.utils;
2
+
3
+ import android.media.MediaMetadataRetriever;
4
+ import android.util.Log;
5
+
6
+ import java.io.IOException;
7
+ import java.util.HashMap;
8
+
9
+ public class MediaMetadataUtil {
10
+
11
+ private static final String TAG = "MediaMetadataUtil";
12
+
13
+ // Function to return MediaMetadataRetriever or null
14
+ public static MediaMetadataRetriever getMediaMetadataRetriever(String source) {
15
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
16
+ try {
17
+ if (source.startsWith("http://") || source.startsWith("https://")) {
18
+ retriever.setDataSource(source, new HashMap<>());
19
+ } else {
20
+ retriever.setDataSource(source);
21
+ }
22
+ return retriever;
23
+ } catch (Exception e) {
24
+ Log.e(TAG, "Error setting data source", e);
25
+ try {
26
+ retriever.release();
27
+ } catch (Exception ee) {
28
+ Log.e(TAG, "Error releasing retriever", ee);
29
+ }
30
+ return null;
31
+ }
32
+ }
33
+
34
+ public static void checkFileValidity(String urlString, FileValidityCallback callback) {
35
+ new Thread(() -> {
36
+ boolean isValid = false;
37
+ String fileType = "unknown";
38
+ long duration;
39
+ MediaMetadataRetriever retriever = getMediaMetadataRetriever(urlString);
40
+ if (retriever == null) {
41
+ callback.onResult(false, fileType, -1L);
42
+ return;
43
+ }
44
+
45
+ // Retrieve the duration
46
+ String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
47
+ duration = durationStr == null ? -1L : Long.parseLong(durationStr);
48
+
49
+ // Determine the type
50
+ String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
51
+ if (hasVideo != null && hasVideo.equals("yes")) {
52
+ fileType = "video";
53
+ isValid = true;
54
+ } else {
55
+ String hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO);
56
+ if (hasAudio != null && hasAudio.equals("yes")) {
57
+ fileType = "audio";
58
+ isValid = true;
59
+ }
60
+ }
61
+
62
+ try {
63
+ retriever.release();
64
+ } catch (IOException e) {
65
+ Log.e(TAG, "Error releasing retriever", e);
66
+ }
67
+ callback.onResult(isValid, fileType, isValid ? duration : -1L);
68
+ }).start();
69
+ }
70
+
71
+ public interface FileValidityCallback {
72
+ void onResult(boolean isValid, String fileType, Long duration);
73
+ }
74
+ }
75
+
@@ -19,9 +19,9 @@ import java.util.ArrayList;
19
19
  import java.util.List;
20
20
 
21
21
  public class StorageUtil {
22
- public static String getOutputPath(Context context) { // use same extension as inputFile
22
+ public static String getOutputPath(Context context, String mOutputExt) {
23
23
  long timestamp = System.currentTimeMillis() / 1000;
24
- File file = new File(context.getFilesDir(), VideoTrimmerUtil.FILE_PREFIX + "_" + timestamp + ".mp4"); // always use mp4 to prevent any issue with ffmpeg
24
+ File file = new File(context.getFilesDir(), VideoTrimmerUtil.FILE_PREFIX + "_" + timestamp + "." + mOutputExt);
25
25
  return file.getAbsolutePath();
26
26
  }
27
27