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.
- package/README.md +33 -9
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +185 -64
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +2 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +15 -8
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +239 -70
- package/android/src/main/res/drawable/airpodsmax.xml +19 -0
- package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
- package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +51 -4
- package/android/src/main/res/xml/file_paths.xml +5 -0
- package/ios/AssetLoader.swift +99 -0
- package/ios/ErrorCode.swift +16 -0
- package/ios/VideoTrim.mm +4 -2
- package/ios/VideoTrim.swift +380 -167
- package/ios/VideoTrimmer.swift +16 -10
- package/ios/VideoTrimmerViewController.swift +78 -12
- package/lib/commonjs/index.js +20 -57
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +19 -57
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +47 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +56 -66
- package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
157
|
-
- `
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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")
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
349
|
+
@Override
|
|
350
|
+
public void onLog(WritableMap log) {
|
|
243
351
|
sendEvent(getReactApplicationContext(), "onLog", log);
|
|
244
352
|
}
|
|
245
353
|
|
|
246
|
-
@Override
|
|
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
|
}
|
|
@@ -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) {
|
|
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 + ".
|
|
24
|
+
File file = new File(context.getFilesDir(), VideoTrimmerUtil.FILE_PREFIX + "_" + timestamp + "." + mOutputExt);
|
|
25
25
|
return file.getAbsolutePath();
|
|
26
26
|
}
|
|
27
27
|
|