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