react-native-video-trim 2.0.0 → 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 (30) hide show
  1. package/README.md +33 -9
  2. package/android/src/main/AndroidManifest.xml +13 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +185 -64
  4. package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
  5. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +2 -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 +15 -8
  9. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +239 -70
  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 +51 -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/VideoTrim.mm +4 -2
  18. package/ios/VideoTrim.swift +380 -167
  19. package/ios/VideoTrimmer.swift +16 -10
  20. package/ios/VideoTrimmerViewController.swift +78 -12
  21. package/lib/commonjs/index.js +20 -57
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/module/index.js +19 -57
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/typescript/index.d.ts +47 -9
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/index.tsx +56 -66
  29. package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
  30. package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
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)
200
+
201
+ This method is to check if a path is a valid video/audio
202
+
203
+ ## closeEditor()
180
204
 
181
- This method is to check if a path is a actual video and editable. It returns `Promise<boolean>`
205
+ Close Editor
182
206
 
183
207
  ## listFiles()
184
208
  Return array of generated output files in app storage. (`Promise<string[]>`)
@@ -1,4 +1,17 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.videotrim">
2
2
 
3
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>
4
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,13 +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();
227
+ trimmerView.onMediaPause();
150
228
  }
151
229
  }
152
230
 
@@ -155,14 +233,16 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
155
233
  hideDialog();
156
234
  }
157
235
 
158
- @Override public void onStartTrim() {
236
+ @Override
237
+ public void onStartTrim() {
159
238
  sendEvent(getReactApplicationContext(), "onStartTrimming", null);
160
239
  runOnUiThread(() -> {
161
240
  buildDialog();
162
241
  });
163
242
  }
164
243
 
165
- @Override public void onTrimmingProgress(int percentage) {
244
+ @Override
245
+ public void onTrimmingProgress(int percentage) {
166
246
  // prevent onTrimmingProgress is called after onFinishTrim (some rare cases)
167
247
  if (mProgressBar == null) {
168
248
  return;
@@ -176,7 +256,9 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
176
256
  }
177
257
 
178
258
 
179
- @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;
180
262
  runOnUiThread(() -> {
181
263
  WritableMap map = Arguments.createMap();
182
264
  map.putString("outputPath", in);
@@ -184,18 +266,42 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
184
266
  map.putDouble("startTime", (double) startTime);
185
267
  map.putDouble("endTime", (double) endTime);
186
268
  sendEvent(getReactApplicationContext(), "onFinishTrimming", map);
187
- showEditorPromise.resolve(in);
188
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
+ }
189
293
  }
190
294
 
191
- @Override public void onError(String errorMessage) {
295
+ @Override
296
+ public void onError(String errorMessage, ErrorCode errorCode) {
192
297
  WritableMap map = Arguments.createMap();
193
298
  map.putString("message", errorMessage);
299
+ map.putString("errorCode", errorCode.name());
194
300
  sendEvent(getReactApplicationContext(), "onError", map);
195
- this.hideDialog();
196
301
  }
197
302
 
198
- @Override public void onCancel() {
303
+ @Override
304
+ public void onCancel() {
199
305
  if (!enableCancelDialog) {
200
306
  sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
201
307
  hideDialog();
@@ -218,7 +324,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
218
324
  alertDialog.show();
219
325
  }
220
326
 
221
- @Override public void onSave() {
327
+ @Override
328
+ public void onSave() {
222
329
  if (!enableSaveDialog) {
223
330
  trimmerView.onSaveClicked();
224
331
  return;
@@ -239,15 +346,16 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
239
346
  alertDialog.show();
240
347
  }
241
348
 
242
- @Override public void onLog(WritableMap log) {
349
+ @Override
350
+ public void onLog(WritableMap log) {
243
351
  sendEvent(getReactApplicationContext(), "onLog", log);
244
352
  }
245
353
 
246
- @Override public void onStatistics(WritableMap statistics) {
354
+ @Override
355
+ public void onStatistics(WritableMap statistics) {
247
356
  sendEvent(getReactApplicationContext(), "onStatistics", statistics);
248
357
  }
249
358
 
250
- @ReactMethod
251
359
  private void hideDialog() {
252
360
  if (mProgressDialog != null) {
253
361
  if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
@@ -256,7 +364,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
256
364
  }
257
365
 
258
366
  if (alertDialog != null) {
259
- if(alertDialog.isShowing()) {
367
+ if (alertDialog.isShowing()) {
260
368
  alertDialog.dismiss();
261
369
  }
262
370
  alertDialog = null;
@@ -326,41 +434,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
326
434
  }
327
435
  }
328
436
 
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
437
  @ReactMethod
365
438
  private void listFiles(Promise promise) {
366
439
  String[] files = StorageUtil.listFiles(getReactApplicationContext());
@@ -385,4 +458,52 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
385
458
  boolean state = StorageUtil.deleteFile(filePath);
386
459
  promise.resolve(state);
387
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
+ }
388
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,12 +1,13 @@
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 {
6
7
  void onStartTrim();
7
8
  void onTrimmingProgress(int percentage);
8
9
  void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
9
- void onError(String errorMessage);
10
+ void onError(String errorMessage, ErrorCode errorCode);
10
11
  void onCancel();
11
12
  void onSave();
12
13
  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