react-native-video-trim 1.0.24 → 2.1.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 (49) hide show
  1. package/README.md +55 -9
  2. package/android/src/main/AndroidManifest.xml +15 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +191 -63
  4. package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
  5. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +6 -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 +36 -8
  9. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +588 -308
  10. package/android/src/main/res/drawable/airpodsmax.xml +19 -0
  11. package/android/src/main/res/drawable/chevron_compact_left.xml +15 -0
  12. package/android/src/main/res/drawable/chevron_compact_right.xml +15 -0
  13. package/android/src/main/res/drawable/chevron_right_with_bg.xml +13 -0
  14. package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
  15. package/android/src/main/res/drawable/pause_fill.xml +15 -0
  16. package/android/src/main/res/drawable/play_fill.xml +15 -0
  17. package/android/src/main/res/drawable/rounded_progress_indicator.xml +16 -0
  18. package/android/src/main/res/drawable/rounded_yellow_left_background.xml +8 -0
  19. package/android/src/main/res/drawable/rounded_yellow_right_background.xml +8 -0
  20. package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
  21. package/android/src/main/res/drawable/yellow_border.xml +9 -0
  22. package/android/src/main/res/layout/video_trimmer_view.xml +194 -75
  23. package/android/src/main/res/values/colors.xml +15 -13
  24. package/android/src/main/res/xml/file_paths.xml +5 -0
  25. package/ios/AssetLoader.swift +99 -0
  26. package/ios/ErrorCode.swift +16 -0
  27. package/ios/VideoTrim.mm +4 -2
  28. package/ios/VideoTrim.swift +405 -168
  29. package/ios/VideoTrimmer.swift +16 -10
  30. package/ios/VideoTrimmerViewController.swift +79 -13
  31. package/lib/commonjs/index.js +20 -57
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/module/index.js +19 -57
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/typescript/index.d.ts +47 -9
  36. package/lib/typescript/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/index.tsx +56 -66
  39. package/android/src/main/java/com/videotrim/adapters/VideoTrimmerAdapter.java +0 -54
  40. package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +0 -534
  41. package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +0 -33
  42. package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +0 -48
  43. package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
  44. package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
  45. package/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
  46. package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
  47. package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
  48. package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
  49. package/android/src/main/res/layout/video_thumb_item_layout.xml +0 -16
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  <img src="images/ios.gif" width="300" />
7
7
  </div>
8
8
 
9
+ ## Features
10
+ - ✅ Support video and audio
11
+ - ✅ Support local and remote files
12
+
9
13
  ## Installation
10
14
 
11
15
  ```sh
@@ -29,7 +33,13 @@ npx pod-install ios
29
33
  ```
30
34
 
31
35
  ## Usage
32
- **Note that for both Android and iOS you have to try on real device**
36
+
37
+ > [!IMPORTANT]
38
+ > Note that for both Android and iOS you have to try on real device
39
+
40
+ > [!IMPORTANT]
41
+ > If you plan to trim remote file, you must install FFMPEG version from "https" onwards, "min" version won't work. See bottom to know how to install specific FFMPEG version
42
+
33
43
 
34
44
  ```js
35
45
  import { showEditor } from 'react-native-video-trim';
@@ -57,7 +67,7 @@ import {
57
67
  NativeEventEmitter,
58
68
  NativeModules,
59
69
  } from 'react-native';
60
- import { isValidVideo, showEditor } from 'react-native-video-trim';
70
+ import { isValidFile, showEditor } from 'react-native-video-trim';
61
71
  import { launchImageLibrary } from 'react-native-image-picker';
62
72
  import { useEffect } from 'react';
63
73
 
@@ -107,7 +117,7 @@ export default function App() {
107
117
  assetRepresentationMode: 'current',
108
118
  });
109
119
 
110
- isValidVideo(result.assets![0]?.uri || '').then((res) =>
120
+ isValidFile(result.assets![0]?.uri || '').then((res) =>
111
121
  console.log(res)
112
122
  );
113
123
 
@@ -121,7 +131,7 @@ export default function App() {
121
131
  </TouchableOpacity>
122
132
  <TouchableOpacity
123
133
  onPress={() => {
124
- isValidVideo('invalid file path').then((res) => console.log(res));
134
+ isValidFile('invalid file path').then((res) => console.log(res));
125
135
  }}
126
136
  style={{
127
137
  padding: 10,
@@ -152,9 +162,19 @@ Main method to show Video Editor UI.
152
162
  *Params*:
153
163
  - `videoPath`: Path to video file, if this is an invalid path, `onError` event will be fired
154
164
  - `config` (optional):
155
-
156
- - `saveToPhoto` (optional, `default = true`): whether to save video to photo/gallery after editing
157
- - `removeAfterSavedToPhoto` (optional, `default = false`): whether to remove output file from storage after saved to Photo
165
+
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
158
178
  - `maxDuration` (optional): maximum duration for the trimmed video
159
179
  - `minDuration` (optional): minimum duration for the trimmed video
160
180
  - `cancelButtonText` (optional): text of left button in Editor dialog
@@ -176,9 +196,13 @@ If `saveToPhoto = true`, you must ensure that you have request permission to wri
176
196
  - For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
177
197
  - For iOS: you need `NSPhotoLibraryUsageDescription` in Info.plist
178
198
 
179
- ## isValidVideo(videoPath: string)
199
+ ## isValidFile(videoPath: string)
180
200
 
181
- This method is to check if a path is a actual video and editable. It returns `Promise<boolean>`
201
+ This method is to check if a path is a valid video/audio
202
+
203
+ ## closeEditor()
204
+
205
+ Close Editor
182
206
 
183
207
  ## listFiles()
184
208
  Return array of generated output files in app storage. (`Promise<string[]>`)
@@ -226,6 +250,16 @@ useEffect(() => {
226
250
  console.log('onError', event);
227
251
  break;
228
252
  }
253
+ case 'onLog': {
254
+ // FFMPEG logs (while trimming)
255
+ console.log('onLog', event);
256
+ break;
257
+ }
258
+ case 'onStatistics': {
259
+ // FFMPEG stats (while trimming)
260
+ console.log('onStatistics', event);
261
+ break;
262
+ }
229
263
  }
230
264
  });
231
265
 
@@ -264,6 +298,18 @@ FFMPEGKIT_PACKAGE_VERSION=5.1 npx pod-install ios
264
298
  FFMPEGKIT_PACKAGE=full FFMPEGKIT_PACKAGE_VERSION=5.1 npx pod-install ios
265
299
  ```
266
300
 
301
+ # Android: update SDK version
302
+ You can override sdk version to use any version in your `android/build.gradle` > `buildscript` > `ext`
303
+ ```gradle
304
+ buildscript {
305
+ ext {
306
+ VideoTrim_compileSdkVersion = 34
307
+ VideoTrim_minSdkVersion = 26
308
+ VideoTrim_targetSdkVersion = 34
309
+ }
310
+ }
311
+ ```
312
+
267
313
  # Thanks
268
314
  - Android part is created by modified + fix bugs from: https://github.com/iknow4/Android-Video-Trimmer
269
315
  - iOS UI is created from: https://github.com/AndreasVerhoeven/VideoTrimmerControl
@@ -1,2 +1,17 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.videotrim">
2
+
3
+ <uses-permission android:name="android.permission.VIBRATE" />
4
+
5
+ <application>
6
+ <!-- FileProvider setup -->
7
+ <provider
8
+ android:name="androidx.core.content.FileProvider"
9
+ android:authorities="${applicationId}.provider"
10
+ android:exported="false"
11
+ android:grantUriPermissions="true">
12
+ <meta-data
13
+ android:name="android.support.FILE_PROVIDER_PATHS"
14
+ android:resource="@xml/file_paths" />
15
+ </provider>
16
+ </application>
2
17
  </manifest>
@@ -1,13 +1,17 @@
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;
11
15
  import android.view.Gravity;
12
16
  import android.view.ViewGroup;
13
17
  import android.widget.LinearLayout;
@@ -17,8 +21,11 @@ import android.widget.TextView;
17
21
  import androidx.annotation.NonNull;
18
22
  import androidx.annotation.Nullable;
19
23
  import androidx.appcompat.app.AlertDialog;
24
+ import androidx.core.content.FileProvider;
20
25
 
26
+ import com.facebook.react.bridge.ActivityEventListener;
21
27
  import com.facebook.react.bridge.Arguments;
28
+ import com.facebook.react.bridge.BaseActivityEventListener;
22
29
  import com.facebook.react.bridge.LifecycleEventListener;
23
30
  import com.facebook.react.bridge.Promise;
24
31
  import com.facebook.react.bridge.ReactApplicationContext;
@@ -29,15 +36,27 @@ import com.facebook.react.bridge.ReadableMap;
29
36
  import com.facebook.react.bridge.WritableMap;
30
37
  import com.facebook.react.module.annotations.ReactModule;
31
38
  import com.facebook.react.modules.core.DeviceEventManagerModule;
39
+ import com.videotrim.enums.ErrorCode;
32
40
  import com.videotrim.interfaces.VideoTrimListener;
41
+ import com.videotrim.utils.MediaMetadataUtil;
33
42
  import com.videotrim.utils.StorageUtil;
43
+ import com.videotrim.utils.VideoTrimmerUtil;
34
44
  import com.videotrim.widgets.VideoTrimmerView;
35
45
 
46
+ import android.content.Intent;
47
+
48
+ import java.io.File;
49
+ import java.io.FileInputStream;
36
50
  import java.io.IOException;
51
+ import java.io.OutputStream;
52
+ import java.util.Objects;
53
+
37
54
  import iknow.android.utils.BaseUtils;
38
55
 
39
56
  @ReactModule(name = VideoTrimModule.NAME)
40
57
  public class VideoTrimModule extends ReactContextBaseJavaModule implements VideoTrimListener, LifecycleEventListener {
58
+ private static final String TAG = VideoTrimmerUtil.class.getSimpleName();
59
+
41
60
  public static final String NAME = "VideoTrim";
42
61
  private static Boolean isInit = false;
43
62
  private VideoTrimmerView trimmerView;
@@ -46,7 +65,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
46
65
  private ProgressBar mProgressBar;
47
66
  private int listenerCount = 0;
48
67
 
49
- private Promise showEditorPromise;
50
68
 
51
69
  private boolean enableCancelDialog = true;
52
70
  private String cancelDialogTitle = "Warning!";
@@ -57,11 +75,65 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
57
75
  private String saveDialogTitle = "Confirmation!";
58
76
  private String saveDialogMessage = "Are you sure want to save?";
59
77
  private String saveDialogCancelText = "Close";
60
- private String saveDialogConfirmText = "Proceed";
78
+ private String saveDialogConfirmText = "Proceed";
61
79
  private String trimmingText = "Trimming video...";
80
+ private String outputFile;
81
+ private boolean saveToPhoto = false;
82
+ private boolean removeAfterSavedToPhoto = false;
83
+ private boolean removeAfterFailedToSavePhoto = false;
84
+ private boolean removeAfterSavedToDocuments = false;
85
+ 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
88
+ private boolean openDocumentsOnFinish = false;
89
+ private boolean openShareSheetOnFinish = false;
90
+ private boolean isVideoType = true;
91
+
92
+ private static final int REQUEST_CODE_SAVE_FILE = 1;
62
93
 
63
94
  public VideoTrimModule(ReactApplicationContext reactContext) {
64
95
  super(reactContext);
96
+ ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
97
+
98
+ @Override
99
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
100
+ if (requestCode == REQUEST_CODE_SAVE_FILE && resultCode == RESULT_OK) {
101
+ Uri uri = intent.getData();
102
+ if (uri == null) {
103
+ return;
104
+ }
105
+ try {
106
+ OutputStream outputStream = reactContext.getContentResolver().openOutputStream(uri);
107
+ if (outputStream == null) {
108
+ return;
109
+ }
110
+ FileInputStream fileInputStream = new FileInputStream(outputFile);
111
+ byte[] buffer = new byte[1024];
112
+ int length;
113
+ while ((length = fileInputStream.read(buffer)) > 0) {
114
+ outputStream.write(buffer, 0, length);
115
+ }
116
+ outputStream.close();
117
+ fileInputStream.close();
118
+ // File saved successfully
119
+ Log.d(TAG, "File saved successfully to " + uri);
120
+ if (removeAfterSavedToDocuments) {
121
+ StorageUtil.deleteFile(outputFile);
122
+ }
123
+ } catch (Exception e) {
124
+ e.printStackTrace();
125
+ // Handle the error
126
+ onError("Failed to save edited video to Documents: " + e.getLocalizedMessage(), ErrorCode.FAIL_TO_SAVE_TO_DOCUMENTS);
127
+ if (removeAfterFailedToSaveDocuments) {
128
+ StorageUtil.deleteFile(outputFile);
129
+ }
130
+ } finally {
131
+ hideDialog();
132
+ }
133
+ }
134
+ }
135
+ };
136
+ reactContext.addActivityEventListener(mActivityEventListener);
65
137
  }
66
138
 
67
139
  @Override
@@ -72,32 +144,37 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
72
144
 
73
145
 
74
146
  @ReactMethod
75
- public void showEditor(String videoPath, ReadableMap config, Promise promise) {
76
- showEditorPromise = promise;
147
+ public void showEditor(String videoPath, ReadableMap config) {
77
148
  if (trimmerView != null || alertDialog != null) {
78
149
  return;
79
150
  }
80
151
 
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
- }
87
-
88
- enableCancelDialog = config.hasKey("enableCancelDialog") ? config.getBoolean("enableCancelDialog") : true;
152
+ enableCancelDialog = !config.hasKey("enableCancelDialog") || config.getBoolean("enableCancelDialog");
89
153
  cancelDialogTitle = config.hasKey("cancelDialogTitle") ? config.getString("cancelDialogTitle") : "Warning!";
90
154
  cancelDialogMessage = config.hasKey("cancelDialogMessage") ? config.getString("cancelDialogMessage") : "Are you sure want to cancel?";
91
155
  cancelDialogCancelText = config.hasKey("cancelDialogCancelText") ? config.getString("cancelDialogCancelText") : "Close";
92
156
  cancelDialogConfirmText = config.hasKey("cancelDialogConfirmText") ? config.getString("cancelDialogConfirmText") : "Proceed";
93
157
 
94
- enableSaveDialog = config.hasKey("enableSaveDialog") ? config.getBoolean("enableSaveDialog") : true;
158
+ enableSaveDialog = !config.hasKey("enableSaveDialog") || config.getBoolean("enableSaveDialog");
95
159
  saveDialogTitle = config.hasKey("saveDialogTitle") ? config.getString("saveDialogTitle") : "Confirmation!";
96
160
  saveDialogMessage = config.hasKey("saveDialogMessage") ? config.getString("saveDialogMessage") : "Are you sure want to save?";
97
161
  saveDialogCancelText = config.hasKey("saveDialogCancelText") ? config.getString("saveDialogCancelText") : "Close";
98
162
  saveDialogConfirmText = config.hasKey("saveDialogConfirmText") ? config.getString("saveDialogConfirmText") : "Proceed";
99
163
  trimmingText = config.hasKey("trimmingText") ? config.getString("trimmingText") : "Trimming video...";
100
164
 
165
+ saveToPhoto = config.hasKey("saveToPhoto") && config.getBoolean("saveToPhoto");
166
+ removeAfterSavedToPhoto = config.hasKey("removeAfterSavedToPhoto") && config.getBoolean("removeAfterSavedToPhoto");
167
+ removeAfterFailedToSavePhoto = config.hasKey("removeAfterFailedToSavePhoto") && config.getBoolean("removeAfterFailedToSavePhoto");
168
+ removeAfterSavedToDocuments = config.hasKey("removeAfterSavedToDocuments") && config.getBoolean("removeAfterSavedToDocuments");
169
+ removeAfterFailedToSaveDocuments = config.hasKey("removeAfterFailedToSaveDocuments") && config.getBoolean("removeAfterFailedToSaveDocuments");
170
+ removeAfterShared = config.hasKey("removeAfterShared") && config.getBoolean("removeAfterShared");
171
+ removeAfterFailedToShare = config.hasKey("removeAfterFailedToShare") && config.getBoolean("removeAfterFailedToShare");
172
+ openDocumentsOnFinish = config.hasKey("openDocumentsOnFinish") && config.getBoolean("openDocumentsOnFinish");
173
+ openShareSheetOnFinish = config.hasKey("openShareSheetOnFinish") && config.getBoolean("openShareSheetOnFinish");
174
+
175
+ isVideoType = !config.hasKey("type") || !Objects.equals(config.getString("type"), "audio");
176
+
177
+
101
178
  Activity activity = getReactApplicationContext().getCurrentActivity();
102
179
 
103
180
  if (!isInit) {
@@ -110,7 +187,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
110
187
  runOnUiThread(() -> {
111
188
  trimmerView = new VideoTrimmerView(getReactApplicationContext(), config, null);
112
189
  trimmerView.setOnTrimVideoListener(this);
113
- trimmerView.initVideoByURI(Uri.parse(videoPath));
190
+ trimmerView.initByURI(Uri.parse(videoPath));
114
191
 
115
192
  AlertDialog.Builder builder = new AlertDialog.Builder(activity, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
116
193
  builder.setCancelable(false);
@@ -140,14 +217,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
140
217
 
141
218
  @Override
142
219
  public void onHostResume() {
143
-
220
+ Log.d(TAG, "onHostResume: ");
144
221
  }
145
222
 
146
223
  @Override
147
224
  public void onHostPause() {
225
+ Log.d(TAG, "onHostPause: ");
148
226
  if (trimmerView != null) {
149
- trimmerView.onVideoPause();
150
- trimmerView.setRestoreState(true);
227
+ trimmerView.onMediaPause();
151
228
  }
152
229
  }
153
230
 
@@ -156,14 +233,16 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
156
233
  hideDialog();
157
234
  }
158
235
 
159
- @Override public void onStartTrim() {
236
+ @Override
237
+ public void onStartTrim() {
160
238
  sendEvent(getReactApplicationContext(), "onStartTrimming", null);
161
239
  runOnUiThread(() -> {
162
240
  buildDialog();
163
241
  });
164
242
  }
165
243
 
166
- @Override public void onTrimmingProgress(int percentage) {
244
+ @Override
245
+ public void onTrimmingProgress(int percentage) {
167
246
  // prevent onTrimmingProgress is called after onFinishTrim (some rare cases)
168
247
  if (mProgressBar == null) {
169
248
  return;
@@ -177,7 +256,9 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
177
256
  }
178
257
 
179
258
 
180
- @Override public void onFinishTrim(String in, long startTime, long endTime, int duration) {
259
+ @Override
260
+ public void onFinishTrim(String in, long startTime, long endTime, int duration) {
261
+ outputFile = in;
181
262
  runOnUiThread(() -> {
182
263
  WritableMap map = Arguments.createMap();
183
264
  map.putString("outputPath", in);
@@ -185,18 +266,42 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
185
266
  map.putDouble("startTime", (double) startTime);
186
267
  map.putDouble("endTime", (double) endTime);
187
268
  sendEvent(getReactApplicationContext(), "onFinishTrimming", map);
188
- showEditorPromise.resolve(in);
189
269
  });
270
+
271
+ if (saveToPhoto && isVideoType) {
272
+ try {
273
+ StorageUtil.saveVideoToGallery(getReactApplicationContext(), in);
274
+ Log.d(TAG, "Edited video saved to Photo Library successfully.");
275
+ if (removeAfterSavedToPhoto) {
276
+ StorageUtil.deleteFile(in);
277
+ }
278
+ } catch (IOException e) {
279
+ e.printStackTrace();
280
+ onError("Failed to save edited video to Photo Library: " + e.getLocalizedMessage(), ErrorCode.FAIL_TO_SAVE_TO_PHOTO);
281
+ if (removeAfterFailedToSavePhoto) {
282
+ StorageUtil.deleteFile(in);
283
+ }
284
+ } finally {
285
+ hideDialog();
286
+ }
287
+ } else if (openDocumentsOnFinish) {
288
+ saveFileToExternalStorage(new File(in));
289
+ } else if (openShareSheetOnFinish) {
290
+ hideDialog();
291
+ shareFile(getReactApplicationContext(), new File(in));
292
+ }
190
293
  }
191
294
 
192
- @Override public void onError(String errorMessage) {
295
+ @Override
296
+ public void onError(String errorMessage, ErrorCode errorCode) {
193
297
  WritableMap map = Arguments.createMap();
194
298
  map.putString("message", errorMessage);
299
+ map.putString("errorCode", errorCode.name());
195
300
  sendEvent(getReactApplicationContext(), "onError", map);
196
- this.hideDialog();
197
301
  }
198
302
 
199
- @Override public void onCancel() {
303
+ @Override
304
+ public void onCancel() {
200
305
  if (!enableCancelDialog) {
201
306
  sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
202
307
  hideDialog();
@@ -219,7 +324,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
219
324
  alertDialog.show();
220
325
  }
221
326
 
222
- @Override public void onSave() {
327
+ @Override
328
+ public void onSave() {
223
329
  if (!enableSaveDialog) {
224
330
  trimmerView.onSaveClicked();
225
331
  return;
@@ -240,7 +346,16 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
240
346
  alertDialog.show();
241
347
  }
242
348
 
243
- @ReactMethod
349
+ @Override
350
+ public void onLog(WritableMap log) {
351
+ sendEvent(getReactApplicationContext(), "onLog", log);
352
+ }
353
+
354
+ @Override
355
+ public void onStatistics(WritableMap statistics) {
356
+ sendEvent(getReactApplicationContext(), "onStatistics", statistics);
357
+ }
358
+
244
359
  private void hideDialog() {
245
360
  if (mProgressDialog != null) {
246
361
  if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
@@ -249,7 +364,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
249
364
  }
250
365
 
251
366
  if (alertDialog != null) {
252
- if(alertDialog.isShowing()) {
367
+ if (alertDialog.isShowing()) {
253
368
  alertDialog.dismiss();
254
369
  }
255
370
  alertDialog = null;
@@ -319,41 +434,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
319
434
  }
320
435
  }
321
436
 
322
-
323
- public boolean isValidVideo(String filePath) {
324
- MediaMetadataRetriever retriever = new MediaMetadataRetriever();
325
-
326
- try {
327
- retriever.setDataSource(getReactApplicationContext(), Uri.parse(filePath));
328
- } catch (Exception e){
329
- e.printStackTrace();
330
- return false;
331
- }
332
-
333
- String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
334
- return "yes".equals(hasVideo);
335
- }
336
-
337
- @ReactMethod
338
- private void isValidVideo(String filePath, Promise promise) {
339
- promise.resolve(isValidVideo(filePath));
340
- }
341
-
342
- @ReactMethod
343
- private void saveVideo(String filePath, Promise promise) {
344
- try {
345
- StorageUtil.saveVideoToGallery(getReactApplicationContext(), filePath);
346
- } catch (IOException e) {
347
- e.printStackTrace();
348
- WritableMap mapE = Arguments.createMap();
349
- mapE.putString("message", "Fail while copying file to Gallery");
350
- sendEvent(getReactApplicationContext(), "onError", mapE);
351
- }
352
-
353
- this.hideDialog();
354
- promise.resolve(null);
355
- }
356
-
357
437
  @ReactMethod
358
438
  private void listFiles(Promise promise) {
359
439
  String[] files = StorageUtil.listFiles(getReactApplicationContext());
@@ -378,4 +458,52 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
378
458
  boolean state = StorageUtil.deleteFile(filePath);
379
459
  promise.resolve(state);
380
460
  }
461
+
462
+ @ReactMethod
463
+ private void closeEditor() {
464
+ hideDialog();
465
+ }
466
+
467
+ @ReactMethod
468
+ private void isValidFile(String filePath, Promise promise) {
469
+ MediaMetadataUtil.checkFileValidity(filePath, (isValid, fileType, duration) -> {
470
+ if (isValid) {
471
+ System.out.println("Valid " + fileType + " file with duration: " + duration + " milliseconds");
472
+ } else {
473
+ System.out.println("Invalid file");
474
+ }
475
+
476
+ WritableMap map = Arguments.createMap();
477
+ map.putBoolean("isValid", isValid);
478
+ map.putString("fileType", fileType);
479
+ map.putDouble("duration", duration);
480
+ promise.resolve(map);
481
+ });
482
+ }
483
+
484
+ private void saveFileToExternalStorage(File file) {
485
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
486
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
487
+ intent.setType("*/*"); // Change MIME type as needed
488
+ intent.putExtra(Intent.EXTRA_TITLE, file.getName());
489
+ getReactApplicationContext().getCurrentActivity().startActivityForResult(intent, REQUEST_CODE_SAVE_FILE);
490
+ }
491
+
492
+ public void shareFile(Context context, File file) {
493
+ Uri fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
494
+
495
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
496
+ shareIntent.setType("*/*");
497
+ shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
498
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
499
+
500
+ // Grant permissions to all applications that can handle the intent
501
+ for (ResolveInfo resolveInfo : context.getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY)) {
502
+ String packageName = resolveInfo.activityInfo.packageName;
503
+ context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
504
+ }
505
+
506
+ // directly use context.startActivity(shareIntent) will cause crash
507
+ getReactApplicationContext().getCurrentActivity().startActivity(Intent.createChooser(shareIntent, "Share file"));
508
+ }
381
509
  }
@@ -0,0 +1,11 @@
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_AUDIO,
8
+ FAIL_TO_LOAD_VIDEO,
9
+ FAIL_TO_SAVE_TO_PHOTO,
10
+ FAIL_TO_SAVE_TO_DOCUMENTS
11
+ }
@@ -1,10 +1,15 @@
1
1
  package com.videotrim.interfaces;
2
2
 
3
+ import com.facebook.react.bridge.WritableMap;
4
+ import com.videotrim.enums.ErrorCode;
5
+
3
6
  public interface VideoTrimListener {
4
7
  void onStartTrim();
5
8
  void onTrimmingProgress(int percentage);
6
9
  void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
7
- void onError(String errorMessage);
10
+ void onError(String errorMessage, ErrorCode errorCode);
8
11
  void onCancel();
9
12
  void onSave();
13
+ void onLog(WritableMap log);
14
+ void onStatistics(WritableMap statistics);
10
15
  }
@@ -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