react-native-video-trim 2.1.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 +147 -36
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +107 -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 +1 -1
- package/src/index.tsx +176 -3
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
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
|
+
<img src="images/document_picker.png" width="300" />
|
|
17
|
+
<img src="images/share_sheet.png" width="300" />
|
|
12
18
|
|
|
13
19
|
## Installation
|
|
14
20
|
|
|
@@ -76,6 +82,11 @@ export default function App() {
|
|
|
76
82
|
const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
|
|
77
83
|
const subscription = eventEmitter.addListener('VideoTrim', (event) => {
|
|
78
84
|
switch (event.name) {
|
|
85
|
+
case 'onLoad': {
|
|
86
|
+
// on media loaded successfully
|
|
87
|
+
console.log('onLoadListener', event);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
79
90
|
case 'onShow': {
|
|
80
91
|
console.log('onShowListener', event);
|
|
81
92
|
break;
|
|
@@ -96,10 +107,22 @@ export default function App() {
|
|
|
96
107
|
console.log('onCancelTrimming', event);
|
|
97
108
|
break;
|
|
98
109
|
}
|
|
110
|
+
case 'onCancel': {
|
|
111
|
+
console.log('onCancel', event);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
99
114
|
case 'onError': {
|
|
100
115
|
console.log('onError', event);
|
|
101
116
|
break;
|
|
102
117
|
}
|
|
118
|
+
case 'onLog': {
|
|
119
|
+
console.log('onLog', event);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'onStatistics': {
|
|
123
|
+
console.log('onStatistics', event);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
103
126
|
}
|
|
104
127
|
});
|
|
105
128
|
|
|
@@ -161,41 +184,83 @@ Main method to show Video Editor UI.
|
|
|
161
184
|
|
|
162
185
|
*Params*:
|
|
163
186
|
- `videoPath`: Path to video file, if this is an invalid path, `onError` event will be fired
|
|
164
|
-
- `config` (optional):
|
|
187
|
+
- `config` (optional, every sub props of `config` is optional):
|
|
165
188
|
|
|
166
|
-
- `type` (
|
|
167
|
-
- `outputExt` (
|
|
168
|
-
- `enableHapticFeedback` (
|
|
169
|
-
- `saveToPhoto` (
|
|
170
|
-
- `openDocumentsOnFinish` (
|
|
171
|
-
- `openShareSheetOnFinish` (
|
|
172
|
-
- `removeAfterSavedToPhoto` (
|
|
173
|
-
- `removeAfterFailedToSavePhoto` (
|
|
174
|
-
- `removeAfterSavedToDocuments` (
|
|
175
|
-
- `removeAfterFailedToSaveDocuments` (
|
|
176
|
-
- `removeAfterShared` (
|
|
177
|
-
- `removeAfterFailedToShare` (
|
|
189
|
+
- `type` (`default = video`): which player to use, `video` or `audio`
|
|
190
|
+
- `outputExt` (`default = mp4`): output file extension
|
|
191
|
+
- `enableHapticFeedback` (`default = true`): whether to enable haptic feedback
|
|
192
|
+
- `saveToPhoto` (Video-only, `default = false`): whether to save video to photo/gallery after editing
|
|
193
|
+
- `openDocumentsOnFinish` (`default = false`): open Document Picker on done trimming
|
|
194
|
+
- `openShareSheetOnFinish` (`default = false`): open Share Sheet on done trimming
|
|
195
|
+
- `removeAfterSavedToPhoto` (`default = false`): whether to remove output file from storage after saved to Photo successfully
|
|
196
|
+
- `removeAfterFailedToSavePhoto` (`default = false`): whether to remove output file if fail to save to Photo
|
|
197
|
+
- `removeAfterSavedToDocuments` (`default = false`): whether to remove output file from storage after saved Documents successfully
|
|
198
|
+
- `removeAfterFailedToSaveDocuments` (`default = false`): whether to remove output file from storage after fail to save to Documents
|
|
199
|
+
- `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)
|
|
200
|
+
- `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
201
|
- `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
|
-
-
|
|
202
|
+
- `minDuration` (`default = 1000`): minimum duration for the trimmed video
|
|
203
|
+
- `cancelButtonText` (`default= "Cancel"`): text of left button in Editor dialog
|
|
204
|
+
- `saveButtonText` (`default= "Save"`): text of right button in Editor dialog
|
|
205
|
+
- `enableCancelDialog` (`default = true`): whether to show alert dialog on press Cancel
|
|
206
|
+
- `cancelDialogTitle` (`default = "Warning!"`)
|
|
207
|
+
- `cancelDialogMessage` (`default = "Are you sure want to cancel?"`)
|
|
208
|
+
- `cancelDialogCancelText` (`default = "Close"`)
|
|
209
|
+
- `cancelDialogConfirmText` (`default = "Proceed"`)
|
|
210
|
+
- `enableSaveDialog` (`default = true`): whether to show alert dialog on press Save
|
|
211
|
+
- `saveDialogTitle` (`default = "Confirmation!"`)
|
|
212
|
+
- `saveDialogMessage` (`default = "Are you sure want to save?"`)
|
|
213
|
+
- `saveDialogCancelText` (`default = "Close"`)
|
|
214
|
+
- `saveDialogConfirmText` (`default = "Proceed"`)
|
|
215
|
+
- `fullScreenModalIOS` (`default = false`): whether to open editor in fullscreen modal
|
|
216
|
+
- `trimmingText` (`default = "Trimming video..."`): trimming text on the progress dialog
|
|
217
|
+
- `autoplay` (`default = false`): whether to autoplay media on load
|
|
218
|
+
- `jumpToPositionOnLoad` (optional): which time position should jump on media loaded (millisecond)
|
|
219
|
+
- `closeWhenFinish` (`default = true`): should editor close on finish trimming
|
|
220
|
+
- `enableCancelTrimming` (`default = true`): enable cancel trimming
|
|
221
|
+
- `cancelTrimmingButtonText` (`default = "Cancel"`)
|
|
222
|
+
- `enableCancelTrimmingDialog` (`default = true`)
|
|
223
|
+
- `cancelTrimmingDialogTitle` (`default = "Warning!"`)
|
|
224
|
+
- `cancelTrimmingDialogMessage` (`default = "Are you sure want to cancel trimming?"`)
|
|
225
|
+
- `cancelTrimmingDialogCancelText` (`default = "Close"`)
|
|
226
|
+
- `cancelTrimmingDialogConfirmText` (`default = "Proceed"`)
|
|
227
|
+
- `headerText` (optional)
|
|
228
|
+
- `headerTextSize` (`default = 16`)
|
|
229
|
+
- `headerTextColor` (`default = white`)
|
|
230
|
+
- `alertOnFailToLoad` (`default = true`)
|
|
231
|
+
- `alertOnFailTitle` (`default = "Error"`)
|
|
232
|
+
- `alertOnFailMessage` (`default = "Fail to load media. Possibly invalid file or no network connection"`)
|
|
233
|
+
- `alertOnFailCloseText` (`default = "Close"`)
|
|
194
234
|
|
|
195
235
|
If `saveToPhoto = true`, you must ensure that you have request permission to write to photo/gallery
|
|
196
236
|
- For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
|
|
197
237
|
- For iOS: you need `NSPhotoLibraryUsageDescription` in Info.plist
|
|
198
238
|
|
|
239
|
+
If `openShareSheetOnFinish=true`, on Android you'll need to update `AndroidManifest.xml` like below:
|
|
240
|
+
```xml
|
|
241
|
+
</application>
|
|
242
|
+
...
|
|
243
|
+
<provider
|
|
244
|
+
android:name="androidx.core.content.FileProvider"
|
|
245
|
+
android:authorities="${applicationId}.provider"
|
|
246
|
+
android:exported="false"
|
|
247
|
+
android:grantUriPermissions="true">
|
|
248
|
+
<meta-data
|
|
249
|
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
250
|
+
android:resource="@xml/file_paths" />
|
|
251
|
+
</provider>
|
|
252
|
+
</application>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
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:
|
|
256
|
+
```xml
|
|
257
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
258
|
+
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
|
259
|
+
<files-path name="internal_files" path="." />
|
|
260
|
+
<external-path name="external_files" path="." />
|
|
261
|
+
</paths>
|
|
262
|
+
```
|
|
263
|
+
|
|
199
264
|
## isValidFile(videoPath: string)
|
|
200
265
|
|
|
201
266
|
This method is to check if a path is a valid video/audio
|
|
@@ -220,43 +285,43 @@ useEffect(() => {
|
|
|
220
285
|
const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
|
|
221
286
|
const subscription = eventEmitter.addListener('VideoTrim', (event) => {
|
|
222
287
|
switch (event.name) {
|
|
288
|
+
case 'onLoad': {
|
|
289
|
+
console.log('onLoadListener', event);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
223
292
|
case 'onShow': {
|
|
224
|
-
// on Dialog show
|
|
225
293
|
console.log('onShowListener', event);
|
|
226
294
|
break;
|
|
227
295
|
}
|
|
228
296
|
case 'onHide': {
|
|
229
|
-
// on Dialog hide
|
|
230
297
|
console.log('onHide', event);
|
|
231
298
|
break;
|
|
232
299
|
}
|
|
233
300
|
case 'onStartTrimming': {
|
|
234
|
-
// on start trimming
|
|
235
301
|
console.log('onStartTrimming', event);
|
|
236
302
|
break;
|
|
237
303
|
}
|
|
238
304
|
case 'onFinishTrimming': {
|
|
239
|
-
// on trimming is done
|
|
240
305
|
console.log('onFinishTrimming', event);
|
|
241
306
|
break;
|
|
242
307
|
}
|
|
243
308
|
case 'onCancelTrimming': {
|
|
244
|
-
// when user clicks Cancel button
|
|
245
309
|
console.log('onCancelTrimming', event);
|
|
246
310
|
break;
|
|
247
311
|
}
|
|
312
|
+
case 'onCancel': {
|
|
313
|
+
console.log('onCancel', event);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
248
316
|
case 'onError': {
|
|
249
|
-
// any error occured: invalid file, lack of permissions to write to photo/gallery, unexpected error...
|
|
250
317
|
console.log('onError', event);
|
|
251
318
|
break;
|
|
252
319
|
}
|
|
253
320
|
case 'onLog': {
|
|
254
|
-
// FFMPEG logs (while trimming)
|
|
255
321
|
console.log('onLog', event);
|
|
256
322
|
break;
|
|
257
323
|
}
|
|
258
324
|
case 'onStatistics': {
|
|
259
|
-
// FFMPEG stats (while trimming)
|
|
260
325
|
console.log('onStatistics', event);
|
|
261
326
|
break;
|
|
262
327
|
}
|
|
@@ -268,6 +333,52 @@ useEffect(() => {
|
|
|
268
333
|
};
|
|
269
334
|
}, []);
|
|
270
335
|
```
|
|
336
|
+
# Audio support
|
|
337
|
+
<img src="images/audio_android.jpg" width="200" />
|
|
338
|
+
<img src="images/audio_ios.jpg" width="200" />
|
|
339
|
+
|
|
340
|
+
For audio only you have to pass `type=audio` and `outputExt`:
|
|
341
|
+
```ts
|
|
342
|
+
showEditor(url, {
|
|
343
|
+
type: 'audio', // important
|
|
344
|
+
outputExt: 'wav', // important: any audio type for output file extension
|
|
345
|
+
})
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
You must install FFMPEG version from "https" onwards, "min" version won't work. Eg.
|
|
349
|
+
```gradle
|
|
350
|
+
// Android: android/build.gradle > buildscript > ext
|
|
351
|
+
|
|
352
|
+
buildscript {
|
|
353
|
+
ext {
|
|
354
|
+
ffmpegKitPackage = "full"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
// iOS:
|
|
359
|
+
|
|
360
|
+
FFMPEGKIT_PACKAGE=https npx pod-install ios
|
|
361
|
+
|
|
362
|
+
// or
|
|
363
|
+
|
|
364
|
+
FFMPEGKIT_PACKAGE=https pod install
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
# Cancel trimming
|
|
368
|
+
<img src="images/progress.jpg" width="200" />
|
|
369
|
+
<img src="images/cancel_confirm.jpg" width="200" />
|
|
370
|
+
|
|
371
|
+
While trimming, you can press Cancel to terminate the process.
|
|
372
|
+
|
|
373
|
+
Related props: `enableCancelTrimming, cancelTrimmingButtonText, enableCancelTrimmingDialog, cancelTrimmingDialogTitle, cancelTrimmingDialogMessage, cancelTrimmingDialogCancelText, cancelTrimmingDialogConfirmText`
|
|
374
|
+
|
|
375
|
+
# Fail to load media
|
|
376
|
+
<img src="images/fail_to_load_media.jpg" width="200" />
|
|
377
|
+
|
|
378
|
+
If there's error while loading media, there'll be a prompt
|
|
379
|
+
|
|
380
|
+
Related props: `alertOnFailToLoad, alertOnFailTitle, alertOnFailMessage, alertOnFailCloseText`
|
|
381
|
+
|
|
271
382
|
# FFMPEG Version
|
|
272
383
|
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
384
|
|
|
@@ -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,21 @@ 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));
|
|
292
319
|
}
|
|
293
320
|
}
|
|
294
321
|
|
|
322
|
+
@Override
|
|
323
|
+
public void onCancelTrim() {
|
|
324
|
+
sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
@Override
|
|
296
328
|
public void onError(String errorMessage, ErrorCode errorCode) {
|
|
297
329
|
WritableMap map = Arguments.createMap();
|
|
@@ -303,8 +335,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
303
335
|
@Override
|
|
304
336
|
public void onCancel() {
|
|
305
337
|
if (!enableCancelDialog) {
|
|
306
|
-
sendEvent(getReactApplicationContext(), "
|
|
307
|
-
hideDialog();
|
|
338
|
+
sendEvent(getReactApplicationContext(), "onCancel", null);
|
|
339
|
+
hideDialog(true);
|
|
308
340
|
return;
|
|
309
341
|
}
|
|
310
342
|
|
|
@@ -314,8 +346,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
314
346
|
builder.setCancelable(false);
|
|
315
347
|
builder.setPositiveButton(cancelDialogConfirmText, (dialog, which) -> {
|
|
316
348
|
dialog.cancel();
|
|
317
|
-
sendEvent(getReactApplicationContext(), "
|
|
318
|
-
hideDialog();
|
|
349
|
+
sendEvent(getReactApplicationContext(), "onCancel", null);
|
|
350
|
+
hideDialog(true);
|
|
319
351
|
});
|
|
320
352
|
builder.setNegativeButton(cancelDialogCancelText, (dialog, which) -> {
|
|
321
353
|
dialog.cancel();
|
|
@@ -356,18 +388,28 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
356
388
|
sendEvent(getReactApplicationContext(), "onStatistics", statistics);
|
|
357
389
|
}
|
|
358
390
|
|
|
359
|
-
private void hideDialog() {
|
|
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
|
+
|
|
360
400
|
if (mProgressDialog != null) {
|
|
361
401
|
if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
|
|
362
402
|
mProgressBar = null;
|
|
363
403
|
mProgressDialog = null;
|
|
364
404
|
}
|
|
365
405
|
|
|
366
|
-
if (
|
|
367
|
-
if (alertDialog
|
|
368
|
-
alertDialog.
|
|
406
|
+
if (shouldCloseEditor) {
|
|
407
|
+
if (alertDialog != null) {
|
|
408
|
+
if (alertDialog.isShowing()) {
|
|
409
|
+
alertDialog.dismiss();
|
|
410
|
+
}
|
|
411
|
+
alertDialog = null;
|
|
369
412
|
}
|
|
370
|
-
alertDialog = null;
|
|
371
413
|
}
|
|
372
414
|
}
|
|
373
415
|
|
|
@@ -390,6 +432,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
390
432
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
391
433
|
));
|
|
392
434
|
textView.setText(trimmingText);
|
|
435
|
+
textView.setGravity(Gravity.CENTER);
|
|
393
436
|
textView.setTextSize(18);
|
|
394
437
|
layout.addView(textView);
|
|
395
438
|
|
|
@@ -402,6 +445,49 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
402
445
|
mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
|
|
403
446
|
layout.addView(mProgressBar);
|
|
404
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
|
+
|
|
405
491
|
// Create the AlertDialog
|
|
406
492
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
407
493
|
builder.setCancelable(false);
|
|
@@ -461,7 +547,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
461
547
|
|
|
462
548
|
@ReactMethod
|
|
463
549
|
private void closeEditor() {
|
|
464
|
-
hideDialog();
|
|
550
|
+
hideDialog(true);
|
|
465
551
|
}
|
|
466
552
|
|
|
467
553
|
@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
|
}
|