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.
- package/README.md +55 -9
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +191 -63
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +6 -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 +36 -8
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +588 -308
- package/android/src/main/res/drawable/airpodsmax.xml +19 -0
- package/android/src/main/res/drawable/chevron_compact_left.xml +15 -0
- package/android/src/main/res/drawable/chevron_compact_right.xml +15 -0
- package/android/src/main/res/drawable/chevron_right_with_bg.xml +13 -0
- package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
- package/android/src/main/res/drawable/pause_fill.xml +15 -0
- package/android/src/main/res/drawable/play_fill.xml +15 -0
- package/android/src/main/res/drawable/rounded_progress_indicator.xml +16 -0
- package/android/src/main/res/drawable/rounded_yellow_left_background.xml +8 -0
- package/android/src/main/res/drawable/rounded_yellow_right_background.xml +8 -0
- package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
- package/android/src/main/res/drawable/yellow_border.xml +9 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +194 -75
- package/android/src/main/res/values/colors.xml +15 -13
- 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 +405 -168
- package/ios/VideoTrimmer.swift +16 -10
- package/ios/VideoTrimmerViewController.swift +79 -13
- 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/com/videotrim/adapters/VideoTrimmerAdapter.java +0 -54
- package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +0 -534
- package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +0 -33
- package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +0 -48
- 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/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
- package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
- package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
- package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
- 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
|
-
|
|
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)
|
|
180
200
|
|
|
181
|
-
This method is to check if a path is a
|
|
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
|
|
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,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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
@
|
|
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
|
}
|
|
@@ -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) {
|
|
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
|
|