react-native-video-trim 2.1.0 → 2.2.1
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 +153 -36
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +109 -21
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +1 -2
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +2 -0
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +12 -9
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +118 -58
- package/android/src/main/res/layout/video_trimmer_view.xml +20 -0
- package/ios/AssetLoader.swift +1 -1
- package/ios/ErrorCode.swift +1 -1
- package/ios/ProgressAlertController.swift +100 -0
- package/ios/VideoTrim.swift +130 -48
- package/ios/VideoTrimmerViewController.swift +130 -27
- package/lib/commonjs/index.js +8 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +9 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +168 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/index.tsx +176 -3
- package/ios/VideoTrim.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -4
package/README.md
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
## Features
|
|
10
10
|
- ✅ Support video and audio
|
|
11
11
|
- ✅ Support local and remote files
|
|
12
|
+
- ✅ Save to Photos, Documents and Share to other apps
|
|
13
|
+
- ✅ Check if file is valid video/audio
|
|
14
|
+
- ✅ File operations: list, clean up, delete specific file
|
|
15
|
+
|
|
16
|
+
<div align="left">
|
|
17
|
+
<img src="images/document_picker.png" width="300" />
|
|
18
|
+
<img src="images/share_sheet.png" width="300" />
|
|
19
|
+
</div>
|
|
12
20
|
|
|
13
21
|
## Installation
|
|
14
22
|
|
|
@@ -76,6 +84,11 @@ export default function App() {
|
|
|
76
84
|
const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
|
|
77
85
|
const subscription = eventEmitter.addListener('VideoTrim', (event) => {
|
|
78
86
|
switch (event.name) {
|
|
87
|
+
case 'onLoad': {
|
|
88
|
+
// on media loaded successfully
|
|
89
|
+
console.log('onLoadListener', event);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
79
92
|
case 'onShow': {
|
|
80
93
|
console.log('onShowListener', event);
|
|
81
94
|
break;
|
|
@@ -96,10 +109,22 @@ export default function App() {
|
|
|
96
109
|
console.log('onCancelTrimming', event);
|
|
97
110
|
break;
|
|
98
111
|
}
|
|
112
|
+
case 'onCancel': {
|
|
113
|
+
console.log('onCancel', event);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
99
116
|
case 'onError': {
|
|
100
117
|
console.log('onError', event);
|
|
101
118
|
break;
|
|
102
119
|
}
|
|
120
|
+
case 'onLog': {
|
|
121
|
+
console.log('onLog', event);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'onStatistics': {
|
|
125
|
+
console.log('onStatistics', event);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
103
128
|
}
|
|
104
129
|
});
|
|
105
130
|
|
|
@@ -161,41 +186,83 @@ Main method to show Video Editor UI.
|
|
|
161
186
|
|
|
162
187
|
*Params*:
|
|
163
188
|
- `videoPath`: Path to video file, if this is an invalid path, `onError` event will be fired
|
|
164
|
-
- `config` (optional):
|
|
189
|
+
- `config` (optional, every sub props of `config` is optional):
|
|
165
190
|
|
|
166
|
-
- `type` (
|
|
167
|
-
- `outputExt` (
|
|
168
|
-
- `enableHapticFeedback` (
|
|
169
|
-
- `saveToPhoto` (
|
|
170
|
-
- `openDocumentsOnFinish` (
|
|
171
|
-
- `openShareSheetOnFinish` (
|
|
172
|
-
- `removeAfterSavedToPhoto` (
|
|
173
|
-
- `removeAfterFailedToSavePhoto` (
|
|
174
|
-
- `removeAfterSavedToDocuments` (
|
|
175
|
-
- `removeAfterFailedToSaveDocuments` (
|
|
176
|
-
- `removeAfterShared` (
|
|
177
|
-
- `removeAfterFailedToShare` (
|
|
191
|
+
- `type` (`default = video`): which player to use, `video` or `audio`
|
|
192
|
+
- `outputExt` (`default = mp4`): output file extension
|
|
193
|
+
- `enableHapticFeedback` (`default = true`): whether to enable haptic feedback
|
|
194
|
+
- `saveToPhoto` (Video-only, `default = false`): whether to save video to photo/gallery after editing
|
|
195
|
+
- `openDocumentsOnFinish` (`default = false`): open Document Picker on done trimming
|
|
196
|
+
- `openShareSheetOnFinish` (`default = false`): open Share Sheet on done trimming
|
|
197
|
+
- `removeAfterSavedToPhoto` (`default = false`): whether to remove output file from storage after saved to Photo successfully
|
|
198
|
+
- `removeAfterFailedToSavePhoto` (`default = false`): whether to remove output file if fail to save to Photo
|
|
199
|
+
- `removeAfterSavedToDocuments` (`default = false`): whether to remove output file from storage after saved Documents successfully
|
|
200
|
+
- `removeAfterFailedToSaveDocuments` (`default = false`): whether to remove output file from storage after fail to save to Documents
|
|
201
|
+
- `removeAfterShared` (`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)
|
|
202
|
+
- `removeAfterFailedToShare` (`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
|
|
178
203
|
- `maxDuration` (optional): maximum duration for the trimmed video
|
|
179
|
-
- `minDuration` (
|
|
180
|
-
- `cancelButtonText` (
|
|
181
|
-
- `saveButtonText` (
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
188
|
-
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
-
|
|
204
|
+
- `minDuration` (`default = 1000`): minimum duration for the trimmed video
|
|
205
|
+
- `cancelButtonText` (`default= "Cancel"`): text of left button in Editor dialog
|
|
206
|
+
- `saveButtonText` (`default= "Save"`): text of right button in Editor dialog
|
|
207
|
+
- `enableCancelDialog` (`default = true`): whether to show alert dialog on press Cancel
|
|
208
|
+
- `cancelDialogTitle` (`default = "Warning!"`)
|
|
209
|
+
- `cancelDialogMessage` (`default = "Are you sure want to cancel?"`)
|
|
210
|
+
- `cancelDialogCancelText` (`default = "Close"`)
|
|
211
|
+
- `cancelDialogConfirmText` (`default = "Proceed"`)
|
|
212
|
+
- `enableSaveDialog` (`default = true`): whether to show alert dialog on press Save
|
|
213
|
+
- `saveDialogTitle` (`default = "Confirmation!"`)
|
|
214
|
+
- `saveDialogMessage` (`default = "Are you sure want to save?"`)
|
|
215
|
+
- `saveDialogCancelText` (`default = "Close"`)
|
|
216
|
+
- `saveDialogConfirmText` (`default = "Proceed"`)
|
|
217
|
+
- `fullScreenModalIOS` (`default = false`): whether to open editor in fullscreen modal
|
|
218
|
+
- `trimmingText` (`default = "Trimming video..."`): trimming text on the progress dialog
|
|
219
|
+
- `autoplay` (`default = false`): whether to autoplay media on load
|
|
220
|
+
- `jumpToPositionOnLoad` (optional): which time position should jump on media loaded (millisecond)
|
|
221
|
+
- `closeWhenFinish` (`default = true`): should editor close on finish trimming
|
|
222
|
+
- `enableCancelTrimming` (`default = true`): enable cancel trimming
|
|
223
|
+
- `cancelTrimmingButtonText` (`default = "Cancel"`)
|
|
224
|
+
- `enableCancelTrimmingDialog` (`default = true`)
|
|
225
|
+
- `cancelTrimmingDialogTitle` (`default = "Warning!"`)
|
|
226
|
+
- `cancelTrimmingDialogMessage` (`default = "Are you sure want to cancel trimming?"`)
|
|
227
|
+
- `cancelTrimmingDialogCancelText` (`default = "Close"`)
|
|
228
|
+
- `cancelTrimmingDialogConfirmText` (`default = "Proceed"`)
|
|
229
|
+
- `headerText` (optional)
|
|
230
|
+
- `headerTextSize` (`default = 16`)
|
|
231
|
+
- `headerTextColor` (`default = white`)
|
|
232
|
+
- `alertOnFailToLoad` (`default = true`)
|
|
233
|
+
- `alertOnFailTitle` (`default = "Error"`)
|
|
234
|
+
- `alertOnFailMessage` (`default = "Fail to load media. Possibly invalid file or no network connection"`)
|
|
235
|
+
- `alertOnFailCloseText` (`default = "Close"`)
|
|
194
236
|
|
|
195
237
|
If `saveToPhoto = true`, you must ensure that you have request permission to write to photo/gallery
|
|
196
238
|
- For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
|
|
197
239
|
- For iOS: you need `NSPhotoLibraryUsageDescription` in Info.plist
|
|
198
240
|
|
|
241
|
+
If `openShareSheetOnFinish=true`, on Android you'll need to update `AndroidManifest.xml` like below:
|
|
242
|
+
```xml
|
|
243
|
+
</application>
|
|
244
|
+
...
|
|
245
|
+
<provider
|
|
246
|
+
android:name="androidx.core.content.FileProvider"
|
|
247
|
+
android:authorities="${applicationId}.provider"
|
|
248
|
+
android:exported="false"
|
|
249
|
+
android:grantUriPermissions="true">
|
|
250
|
+
<meta-data
|
|
251
|
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
252
|
+
android:resource="@xml/file_paths" />
|
|
253
|
+
</provider>
|
|
254
|
+
</application>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
If you face issue when building Android app related to `file_paths`, then you may need to create `res/xml/file_paths.xml`: with the following content:
|
|
258
|
+
```xml
|
|
259
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
260
|
+
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
261
|
+
<files-path name="internal_files" path="." />
|
|
262
|
+
<external-path name="external_files" path="." />
|
|
263
|
+
</paths>
|
|
264
|
+
```
|
|
265
|
+
|
|
199
266
|
## isValidFile(videoPath: string)
|
|
200
267
|
|
|
201
268
|
This method is to check if a path is a valid video/audio
|
|
@@ -220,43 +287,43 @@ useEffect(() => {
|
|
|
220
287
|
const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
|
|
221
288
|
const subscription = eventEmitter.addListener('VideoTrim', (event) => {
|
|
222
289
|
switch (event.name) {
|
|
290
|
+
case 'onLoad': {
|
|
291
|
+
console.log('onLoadListener', event);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
223
294
|
case 'onShow': {
|
|
224
|
-
// on Dialog show
|
|
225
295
|
console.log('onShowListener', event);
|
|
226
296
|
break;
|
|
227
297
|
}
|
|
228
298
|
case 'onHide': {
|
|
229
|
-
// on Dialog hide
|
|
230
299
|
console.log('onHide', event);
|
|
231
300
|
break;
|
|
232
301
|
}
|
|
233
302
|
case 'onStartTrimming': {
|
|
234
|
-
// on start trimming
|
|
235
303
|
console.log('onStartTrimming', event);
|
|
236
304
|
break;
|
|
237
305
|
}
|
|
238
306
|
case 'onFinishTrimming': {
|
|
239
|
-
// on trimming is done
|
|
240
307
|
console.log('onFinishTrimming', event);
|
|
241
308
|
break;
|
|
242
309
|
}
|
|
243
310
|
case 'onCancelTrimming': {
|
|
244
|
-
// when user clicks Cancel button
|
|
245
311
|
console.log('onCancelTrimming', event);
|
|
246
312
|
break;
|
|
247
313
|
}
|
|
314
|
+
case 'onCancel': {
|
|
315
|
+
console.log('onCancel', event);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
248
318
|
case 'onError': {
|
|
249
|
-
// any error occured: invalid file, lack of permissions to write to photo/gallery, unexpected error...
|
|
250
319
|
console.log('onError', event);
|
|
251
320
|
break;
|
|
252
321
|
}
|
|
253
322
|
case 'onLog': {
|
|
254
|
-
// FFMPEG logs (while trimming)
|
|
255
323
|
console.log('onLog', event);
|
|
256
324
|
break;
|
|
257
325
|
}
|
|
258
326
|
case 'onStatistics': {
|
|
259
|
-
// FFMPEG stats (while trimming)
|
|
260
327
|
console.log('onStatistics', event);
|
|
261
328
|
break;
|
|
262
329
|
}
|
|
@@ -268,6 +335,56 @@ useEffect(() => {
|
|
|
268
335
|
};
|
|
269
336
|
}, []);
|
|
270
337
|
```
|
|
338
|
+
# Audio support
|
|
339
|
+
<div align="left">
|
|
340
|
+
<img src="images/audio_android.jpg" width="200" />
|
|
341
|
+
<img src="images/audio_ios.jpg" width="200" />
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
For audio only you have to pass `type=audio` and `outputExt`:
|
|
345
|
+
```ts
|
|
346
|
+
showEditor(url, {
|
|
347
|
+
type: 'audio', // important
|
|
348
|
+
outputExt: 'wav', // important: any audio type for output file extension
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
You must install FFMPEG version from "https" onwards, "min" version won't work. Eg.
|
|
353
|
+
```gradle
|
|
354
|
+
// Android: android/build.gradle > buildscript > ext
|
|
355
|
+
|
|
356
|
+
buildscript {
|
|
357
|
+
ext {
|
|
358
|
+
ffmpegKitPackage = "full"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
// iOS:
|
|
363
|
+
|
|
364
|
+
FFMPEGKIT_PACKAGE=https npx pod-install ios
|
|
365
|
+
|
|
366
|
+
// or
|
|
367
|
+
|
|
368
|
+
FFMPEGKIT_PACKAGE=https pod install
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
# Cancel trimming
|
|
372
|
+
<div align="left">
|
|
373
|
+
<img src="images/progress.jpg" width="200" />
|
|
374
|
+
<img src="images/cancel_confirm.jpg" width="200" />
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
While trimming, you can press Cancel to terminate the process.
|
|
378
|
+
|
|
379
|
+
Related props: `enableCancelTrimming, cancelTrimmingButtonText, enableCancelTrimmingDialog, cancelTrimmingDialogTitle, cancelTrimmingDialogMessage, cancelTrimmingDialogCancelText, cancelTrimmingDialogConfirmText`
|
|
380
|
+
|
|
381
|
+
# Fail to load media
|
|
382
|
+
<img src="images/fail_to_load_media.jpg" width="200" />
|
|
383
|
+
|
|
384
|
+
If there's error while loading media, there'll be a prompt
|
|
385
|
+
|
|
386
|
+
Related props: `alertOnFailToLoad, alertOnFailTitle, alertOnFailMessage, alertOnFailCloseText`
|
|
387
|
+
|
|
271
388
|
# FFMPEG Version
|
|
272
389
|
This library uses FFMPEG-Kit Android under the hood, by default FFMPEG-min is used, which gives smallest bundle size: https://github.com/arthenica/ffmpeg-kit#9-packages
|
|
273
390
|
|
|
@@ -12,8 +12,10 @@ import android.graphics.Color;
|
|
|
12
12
|
import android.net.Uri;
|
|
13
13
|
import android.os.Build;
|
|
14
14
|
import android.util.Log;
|
|
15
|
+
import android.util.TypedValue;
|
|
15
16
|
import android.view.Gravity;
|
|
16
17
|
import android.view.ViewGroup;
|
|
18
|
+
import android.widget.Button;
|
|
17
19
|
import android.widget.LinearLayout;
|
|
18
20
|
import android.widget.ProgressBar;
|
|
19
21
|
import android.widget.TextView;
|
|
@@ -21,6 +23,7 @@ import android.widget.TextView;
|
|
|
21
23
|
import androidx.annotation.NonNull;
|
|
22
24
|
import androidx.annotation.Nullable;
|
|
23
25
|
import androidx.appcompat.app.AlertDialog;
|
|
26
|
+
import androidx.core.content.ContextCompat;
|
|
24
27
|
import androidx.core.content.FileProvider;
|
|
25
28
|
|
|
26
29
|
import com.facebook.react.bridge.ActivityEventListener;
|
|
@@ -62,10 +65,17 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
62
65
|
private VideoTrimmerView trimmerView;
|
|
63
66
|
private AlertDialog alertDialog;
|
|
64
67
|
private AlertDialog mProgressDialog;
|
|
68
|
+
private AlertDialog cancelTrimmingConfirmDialog;
|
|
65
69
|
private ProgressBar mProgressBar;
|
|
66
70
|
private int listenerCount = 0;
|
|
67
|
-
|
|
68
|
-
|
|
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";
|
|
69
79
|
private boolean enableCancelDialog = true;
|
|
70
80
|
private String cancelDialogTitle = "Warning!";
|
|
71
81
|
private String cancelDialogMessage = "Are you sure want to cancel?";
|
|
@@ -83,11 +93,12 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
83
93
|
private boolean removeAfterFailedToSavePhoto = false;
|
|
84
94
|
private boolean removeAfterSavedToDocuments = false;
|
|
85
95
|
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
|
|
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
|
|
88
98
|
private boolean openDocumentsOnFinish = false;
|
|
89
99
|
private boolean openShareSheetOnFinish = false;
|
|
90
100
|
private boolean isVideoType = true;
|
|
101
|
+
private boolean closeWhenFinish = true;
|
|
91
102
|
|
|
92
103
|
private static final int REQUEST_CODE_SAVE_FILE = 1;
|
|
93
104
|
|
|
@@ -128,7 +139,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
128
139
|
StorageUtil.deleteFile(outputFile);
|
|
129
140
|
}
|
|
130
141
|
} finally {
|
|
131
|
-
hideDialog();
|
|
142
|
+
hideDialog(true);
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
}
|
|
@@ -148,6 +159,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
148
159
|
if (trimmerView != null || alertDialog != null) {
|
|
149
160
|
return;
|
|
150
161
|
}
|
|
162
|
+
enableCancelTrimming = !config.hasKey("enableCancelTrimming") || config.getBoolean("enableCancelTrimming");
|
|
163
|
+
|
|
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";
|
|
151
170
|
|
|
152
171
|
enableCancelDialog = !config.hasKey("enableCancelDialog") || config.getBoolean("enableCancelDialog");
|
|
153
172
|
cancelDialogTitle = config.hasKey("cancelDialogTitle") ? config.getString("cancelDialogTitle") : "Warning!";
|
|
@@ -167,13 +186,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
167
186
|
removeAfterFailedToSavePhoto = config.hasKey("removeAfterFailedToSavePhoto") && config.getBoolean("removeAfterFailedToSavePhoto");
|
|
168
187
|
removeAfterSavedToDocuments = config.hasKey("removeAfterSavedToDocuments") && config.getBoolean("removeAfterSavedToDocuments");
|
|
169
188
|
removeAfterFailedToSaveDocuments = config.hasKey("removeAfterFailedToSaveDocuments") && config.getBoolean("removeAfterFailedToSaveDocuments");
|
|
170
|
-
removeAfterShared = config.hasKey("removeAfterShared") && config.getBoolean("removeAfterShared");
|
|
171
|
-
removeAfterFailedToShare = config.hasKey("removeAfterFailedToShare") && config.getBoolean("removeAfterFailedToShare");
|
|
189
|
+
// removeAfterShared = config.hasKey("removeAfterShared") && config.getBoolean("removeAfterShared");
|
|
190
|
+
// removeAfterFailedToShare = config.hasKey("removeAfterFailedToShare") && config.getBoolean("removeAfterFailedToShare");
|
|
172
191
|
openDocumentsOnFinish = config.hasKey("openDocumentsOnFinish") && config.getBoolean("openDocumentsOnFinish");
|
|
173
192
|
openShareSheetOnFinish = config.hasKey("openShareSheetOnFinish") && config.getBoolean("openShareSheetOnFinish");
|
|
174
193
|
|
|
175
194
|
isVideoType = !config.hasKey("type") || !Objects.equals(config.getString("type"), "audio");
|
|
176
195
|
|
|
196
|
+
closeWhenFinish = !config.hasKey("closeWhenFinish") || config.getBoolean("closeWhenFinish");
|
|
177
197
|
|
|
178
198
|
Activity activity = getReactApplicationContext().getCurrentActivity();
|
|
179
199
|
|
|
@@ -202,7 +222,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
202
222
|
trimmerView.onDestroy();
|
|
203
223
|
trimmerView = null;
|
|
204
224
|
}
|
|
205
|
-
hideDialog();
|
|
225
|
+
hideDialog(true);
|
|
206
226
|
sendEvent(getReactApplicationContext(), "onHide", null);
|
|
207
227
|
});
|
|
208
228
|
sendEvent(getReactApplicationContext(), "onShow", null);
|
|
@@ -230,7 +250,14 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
230
250
|
|
|
231
251
|
@Override
|
|
232
252
|
public void onHostDestroy() {
|
|
233
|
-
hideDialog();
|
|
253
|
+
hideDialog(true);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@Override
|
|
257
|
+
public void onLoad(int duration) {
|
|
258
|
+
WritableMap map = Arguments.createMap();
|
|
259
|
+
map.putInt("duration", duration);
|
|
260
|
+
sendEvent(getReactApplicationContext(), "onLoad", map);
|
|
234
261
|
}
|
|
235
262
|
|
|
236
263
|
@Override
|
|
@@ -282,16 +309,23 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
282
309
|
StorageUtil.deleteFile(in);
|
|
283
310
|
}
|
|
284
311
|
} finally {
|
|
285
|
-
hideDialog();
|
|
312
|
+
hideDialog(closeWhenFinish);
|
|
286
313
|
}
|
|
287
314
|
} else if (openDocumentsOnFinish) {
|
|
288
315
|
saveFileToExternalStorage(new File(in));
|
|
289
316
|
} else if (openShareSheetOnFinish) {
|
|
290
|
-
hideDialog();
|
|
317
|
+
hideDialog(closeWhenFinish);
|
|
291
318
|
shareFile(getReactApplicationContext(), new File(in));
|
|
319
|
+
} else {
|
|
320
|
+
hideDialog(closeWhenFinish);
|
|
292
321
|
}
|
|
293
322
|
}
|
|
294
323
|
|
|
324
|
+
@Override
|
|
325
|
+
public void onCancelTrim() {
|
|
326
|
+
sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
|
|
327
|
+
}
|
|
328
|
+
|
|
295
329
|
@Override
|
|
296
330
|
public void onError(String errorMessage, ErrorCode errorCode) {
|
|
297
331
|
WritableMap map = Arguments.createMap();
|
|
@@ -303,8 +337,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
303
337
|
@Override
|
|
304
338
|
public void onCancel() {
|
|
305
339
|
if (!enableCancelDialog) {
|
|
306
|
-
sendEvent(getReactApplicationContext(), "
|
|
307
|
-
hideDialog();
|
|
340
|
+
sendEvent(getReactApplicationContext(), "onCancel", null);
|
|
341
|
+
hideDialog(true);
|
|
308
342
|
return;
|
|
309
343
|
}
|
|
310
344
|
|
|
@@ -314,8 +348,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
314
348
|
builder.setCancelable(false);
|
|
315
349
|
builder.setPositiveButton(cancelDialogConfirmText, (dialog, which) -> {
|
|
316
350
|
dialog.cancel();
|
|
317
|
-
sendEvent(getReactApplicationContext(), "
|
|
318
|
-
hideDialog();
|
|
351
|
+
sendEvent(getReactApplicationContext(), "onCancel", null);
|
|
352
|
+
hideDialog(true);
|
|
319
353
|
});
|
|
320
354
|
builder.setNegativeButton(cancelDialogCancelText, (dialog, which) -> {
|
|
321
355
|
dialog.cancel();
|
|
@@ -356,18 +390,28 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
356
390
|
sendEvent(getReactApplicationContext(), "onStatistics", statistics);
|
|
357
391
|
}
|
|
358
392
|
|
|
359
|
-
private void hideDialog() {
|
|
393
|
+
private void hideDialog(boolean shouldCloseEditor) {
|
|
394
|
+
// handle the case when the cancel dialog is still showing but the trimming is finished
|
|
395
|
+
if (cancelTrimmingConfirmDialog != null) {
|
|
396
|
+
if (cancelTrimmingConfirmDialog.isShowing()) {
|
|
397
|
+
cancelTrimmingConfirmDialog.dismiss();
|
|
398
|
+
}
|
|
399
|
+
cancelTrimmingConfirmDialog = null;
|
|
400
|
+
}
|
|
401
|
+
|
|
360
402
|
if (mProgressDialog != null) {
|
|
361
403
|
if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
|
|
362
404
|
mProgressBar = null;
|
|
363
405
|
mProgressDialog = null;
|
|
364
406
|
}
|
|
365
407
|
|
|
366
|
-
if (
|
|
367
|
-
if (alertDialog
|
|
368
|
-
alertDialog.
|
|
408
|
+
if (shouldCloseEditor) {
|
|
409
|
+
if (alertDialog != null) {
|
|
410
|
+
if (alertDialog.isShowing()) {
|
|
411
|
+
alertDialog.dismiss();
|
|
412
|
+
}
|
|
413
|
+
alertDialog = null;
|
|
369
414
|
}
|
|
370
|
-
alertDialog = null;
|
|
371
415
|
}
|
|
372
416
|
}
|
|
373
417
|
|
|
@@ -390,6 +434,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
390
434
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
391
435
|
));
|
|
392
436
|
textView.setText(trimmingText);
|
|
437
|
+
textView.setGravity(Gravity.CENTER);
|
|
393
438
|
textView.setTextSize(18);
|
|
394
439
|
layout.addView(textView);
|
|
395
440
|
|
|
@@ -402,6 +447,49 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
402
447
|
mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
|
|
403
448
|
layout.addView(mProgressBar);
|
|
404
449
|
|
|
450
|
+
// Create button
|
|
451
|
+
if (enableCancelTrimming) {
|
|
452
|
+
Button button = new Button(activity);
|
|
453
|
+
button.setLayoutParams(new ViewGroup.LayoutParams(
|
|
454
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
455
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
456
|
+
));
|
|
457
|
+
// Set the text and style it like a text button
|
|
458
|
+
button.setText(cancelTrimmingButtonText);
|
|
459
|
+
button.setTextColor(ContextCompat.getColor(activity, android.R.color.holo_red_light)); // or use your custom color
|
|
460
|
+
|
|
461
|
+
// Apply ripple effect while keeping the button background transparent
|
|
462
|
+
TypedValue outValue = new TypedValue();
|
|
463
|
+
activity.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
|
|
464
|
+
button.setBackgroundResource(outValue.resourceId);
|
|
465
|
+
button.setOnClickListener(v -> {
|
|
466
|
+
if (enableCancelTrimmingDialog) {
|
|
467
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
468
|
+
builder.setMessage(cancelTrimmingDialogMessage);
|
|
469
|
+
builder.setTitle(cancelTrimmingDialogTitle);
|
|
470
|
+
builder.setCancelable(false);
|
|
471
|
+
builder.setPositiveButton(cancelTrimmingDialogConfirmText, (dialog, which) -> {
|
|
472
|
+
trimmerView.onCancelTrimClicked();
|
|
473
|
+
|
|
474
|
+
if (mProgressDialog != null && mProgressDialog.isShowing()) {
|
|
475
|
+
mProgressDialog.dismiss();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
builder.setNegativeButton(cancelTrimmingDialogCancelText, (dialog, which) -> {
|
|
479
|
+
dialog.cancel();
|
|
480
|
+
});
|
|
481
|
+
cancelTrimmingConfirmDialog = builder.create();
|
|
482
|
+
cancelTrimmingConfirmDialog.show();
|
|
483
|
+
} else {
|
|
484
|
+
trimmerView.onCancelTrimClicked();
|
|
485
|
+
if (mProgressDialog != null && mProgressDialog.isShowing()) {
|
|
486
|
+
mProgressDialog.dismiss();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
layout.addView(button);
|
|
491
|
+
}
|
|
492
|
+
|
|
405
493
|
// Create the AlertDialog
|
|
406
494
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
407
495
|
builder.setCancelable(false);
|
|
@@ -461,7 +549,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
461
549
|
|
|
462
550
|
@ReactMethod
|
|
463
551
|
private void closeEditor() {
|
|
464
|
-
hideDialog();
|
|
552
|
+
hideDialog(true);
|
|
465
553
|
}
|
|
466
554
|
|
|
467
555
|
@ReactMethod
|
|
@@ -4,9 +4,11 @@ import com.facebook.react.bridge.WritableMap;
|
|
|
4
4
|
import com.videotrim.enums.ErrorCode;
|
|
5
5
|
|
|
6
6
|
public interface VideoTrimListener {
|
|
7
|
+
void onLoad(int duration);
|
|
7
8
|
void onStartTrim();
|
|
8
9
|
void onTrimmingProgress(int percentage);
|
|
9
10
|
void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
|
|
11
|
+
void onCancelTrim();
|
|
10
12
|
void onError(String errorMessage, ErrorCode errorCode);
|
|
11
13
|
void onCancel();
|
|
12
14
|
void onSave();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
package com.videotrim.utils;
|
|
2
2
|
|
|
3
|
+
import android.annotation.SuppressLint;
|
|
3
4
|
import android.graphics.Bitmap;
|
|
4
5
|
import android.media.MediaMetadataRetriever;
|
|
5
6
|
import com.arthenica.ffmpegkit.FFmpegKit;
|
|
7
|
+
import com.arthenica.ffmpegkit.FFmpegSession;
|
|
6
8
|
import com.arthenica.ffmpegkit.ReturnCode;
|
|
7
9
|
import com.arthenica.ffmpegkit.SessionState;
|
|
8
10
|
import com.facebook.react.bridge.Arguments;
|
|
@@ -37,12 +39,12 @@ public class VideoTrimmerUtil {
|
|
|
37
39
|
public static final int THUMB_HEIGHT = UnitConverter.dpToPx(50); // x2 for better resolution
|
|
38
40
|
private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
|
|
39
41
|
|
|
40
|
-
public static
|
|
42
|
+
public static FFmpegSession trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, final VideoTrimListener callback) {
|
|
41
43
|
// Get the current date and time
|
|
42
44
|
Date currentDate = new Date();
|
|
43
45
|
|
|
44
46
|
// Create a SimpleDateFormat object with the desired format
|
|
45
|
-
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
|
47
|
+
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
|
46
48
|
|
|
47
49
|
// Set the timezone to UTC
|
|
48
50
|
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
@@ -65,18 +67,19 @@ public class VideoTrimmerUtil {
|
|
|
65
67
|
"creation_time=" + formattedDateTime,
|
|
66
68
|
outputFile
|
|
67
69
|
};
|
|
68
|
-
System.out.println("
|
|
70
|
+
System.out.println("Command: " + String.join(",", cmds));
|
|
69
71
|
|
|
70
|
-
FFmpegKit.executeWithArgumentsAsync(cmds, session -> {
|
|
72
|
+
return FFmpegKit.executeWithArgumentsAsync(cmds, session -> {
|
|
71
73
|
SessionState state = session.getState();
|
|
72
74
|
ReturnCode returnCode = session.getReturnCode();
|
|
73
|
-
|
|
74
|
-
if (ReturnCode.isSuccess(returnCode)) {
|
|
75
|
+
if (ReturnCode.isSuccess(session.getReturnCode())) {
|
|
75
76
|
// SUCCESS
|
|
76
77
|
callback.onFinishTrim(outputFile, startMs, endMs, videoDuration);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
} else if (ReturnCode.isCancel(session.getReturnCode())) {
|
|
79
|
+
// CANCEL
|
|
80
|
+
callback.onCancelTrim();
|
|
81
|
+
} else {
|
|
82
|
+
// FAILURE
|
|
80
83
|
String errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
|
|
81
84
|
callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED);
|
|
82
85
|
}
|