react-native-video-trim 1.0.0 → 1.0.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/android/build.gradle +1 -1
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +121 -29
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +2 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +13 -21
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +32 -16
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +1 -0
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -91,9 +91,9 @@ dependencies {
|
|
|
91
91
|
//noinspection GradleDynamicVersion
|
|
92
92
|
implementation "com.facebook.react:react-native:+"
|
|
93
93
|
implementation 'com.github.iknow4:android-utils-sdk:1.1.2'
|
|
94
|
-
implementation 'nl.bravobit:android-ffmpeg:1.1.7'
|
|
95
94
|
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
|
96
95
|
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
|
|
96
|
+
implementation 'com.arthenica:ffmpeg-kit-full:5.1.LTS'
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
if (isNewArchitectureEnabled()) {
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
package com.videotrim;
|
|
2
2
|
|
|
3
3
|
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
|
4
|
+
|
|
5
|
+
import android.Manifest;
|
|
4
6
|
import android.app.Activity;
|
|
5
|
-
import android.
|
|
7
|
+
import android.content.res.ColorStateList;
|
|
8
|
+
import android.graphics.Color;
|
|
6
9
|
import android.media.MediaMetadataRetriever;
|
|
7
10
|
import android.net.Uri;
|
|
11
|
+
import android.os.Build;
|
|
12
|
+
import android.view.Gravity;
|
|
13
|
+
import android.view.ViewGroup;
|
|
14
|
+
import android.widget.LinearLayout;
|
|
15
|
+
import android.widget.ProgressBar;
|
|
16
|
+
import android.widget.TextView;
|
|
8
17
|
|
|
9
18
|
import androidx.annotation.NonNull;
|
|
10
19
|
import androidx.annotation.Nullable;
|
|
11
20
|
import androidx.appcompat.app.AlertDialog;
|
|
21
|
+
import androidx.fragment.app.FragmentActivity;
|
|
22
|
+
|
|
12
23
|
import com.facebook.react.bridge.Arguments;
|
|
13
24
|
import com.facebook.react.bridge.LifecycleEventListener;
|
|
14
25
|
import com.facebook.react.bridge.Promise;
|
|
@@ -20,12 +31,14 @@ import com.facebook.react.bridge.ReadableMap;
|
|
|
20
31
|
import com.facebook.react.bridge.WritableMap;
|
|
21
32
|
import com.facebook.react.module.annotations.ReactModule;
|
|
22
33
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
34
|
+
import com.permissionx.guolindev.PermissionX;
|
|
23
35
|
import com.videotrim.interfaces.VideoTrimListener;
|
|
24
36
|
import com.videotrim.utils.StorageUtil;
|
|
25
37
|
import com.videotrim.widgets.VideoTrimmerView;
|
|
38
|
+
|
|
39
|
+
import java.io.File;
|
|
26
40
|
import java.io.IOException;
|
|
27
41
|
import iknow.android.utils.BaseUtils;
|
|
28
|
-
import nl.bravobit.ffmpeg.FFmpeg;
|
|
29
42
|
|
|
30
43
|
@ReactModule(name = VideoTrimModule.NAME)
|
|
31
44
|
public class VideoTrimModule extends ReactContextBaseJavaModule implements VideoTrimListener, LifecycleEventListener {
|
|
@@ -33,7 +46,8 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
33
46
|
private static Boolean isInit = false;
|
|
34
47
|
private VideoTrimmerView trimmerView;
|
|
35
48
|
private AlertDialog alertDialog;
|
|
36
|
-
private
|
|
49
|
+
private AlertDialog mProgressDialog;
|
|
50
|
+
private ProgressBar mProgressBar;
|
|
37
51
|
private Boolean mSaveToPhoto = true;
|
|
38
52
|
private int mMaxDuration = 0;
|
|
39
53
|
private int listenerCount = 0;
|
|
@@ -89,6 +103,17 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
89
103
|
alertDialog.setView(trimmerView);
|
|
90
104
|
alertDialog.show();
|
|
91
105
|
|
|
106
|
+
|
|
107
|
+
if (this.mSaveToPhoto) {
|
|
108
|
+
// some how this is not fired when user first install app and tap Allow
|
|
109
|
+
// so that we just request permission first, and later it'll be able to save to Gallery immediately
|
|
110
|
+
PermissionX.init((FragmentActivity) getReactApplicationContext().getCurrentActivity())
|
|
111
|
+
.permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
112
|
+
.request((allGranted, grantedList, deniedList) -> {
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
92
117
|
// this is to ensure to release resource if dialog is dismissed in unexpected way (Eg. open control/notification center by dragging from top of screen)
|
|
93
118
|
alertDialog.setOnDismissListener(dialog -> {
|
|
94
119
|
// This is called in same thread as the trimmer view -> UI thread
|
|
@@ -107,12 +132,6 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
107
132
|
isInit = true;
|
|
108
133
|
// we have to init this before create videoTrimmerView
|
|
109
134
|
BaseUtils.init(getReactApplicationContext());
|
|
110
|
-
if (!FFmpeg.getInstance(getReactApplicationContext()).isSupported()) {
|
|
111
|
-
// we have to call this for FFMPEG to initialize, otherwise it'll throw can't open ffmpeg (no such file or dir)
|
|
112
|
-
WritableMap mapE = Arguments.createMap();
|
|
113
|
-
mapE.putString("message", "Android CPU arch not supported");
|
|
114
|
-
sendEvent(getReactApplicationContext(), "onError", mapE);
|
|
115
|
-
}
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
@Override
|
|
@@ -136,25 +155,58 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
136
155
|
@Override public void onStartTrim() {
|
|
137
156
|
sendEvent(getReactApplicationContext(), "onStartTrimming", null);
|
|
138
157
|
runOnUiThread(() -> {
|
|
139
|
-
buildDialog(
|
|
158
|
+
buildDialog();
|
|
140
159
|
});
|
|
141
160
|
}
|
|
142
161
|
|
|
162
|
+
@Override public void onTrimmingProgress(int percentage) {
|
|
163
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
164
|
+
mProgressBar.setProgress(percentage, true);
|
|
165
|
+
} else {
|
|
166
|
+
mProgressBar.setProgress(percentage);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
143
171
|
@Override public void onFinishTrim(String in) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
runOnUiThread(() -> {
|
|
173
|
+
if (mSaveToPhoto) {
|
|
174
|
+
PermissionX.init((FragmentActivity) getReactApplicationContext().getCurrentActivity())
|
|
175
|
+
.permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
176
|
+
.request((allGranted, grantedList, deniedList) -> {
|
|
177
|
+
// some how this is not fired when user first tap Allow
|
|
178
|
+
if (allGranted) {
|
|
179
|
+
try {
|
|
180
|
+
StorageUtil.saveVideoToGallery(getReactApplicationContext(), in);
|
|
181
|
+
WritableMap map = Arguments.createMap();
|
|
182
|
+
map.putString("outputPath", in);
|
|
183
|
+
sendEvent(getReactApplicationContext(), "onFinishTrimming", map);
|
|
184
|
+
} catch (IOException e) {
|
|
185
|
+
e.printStackTrace();
|
|
186
|
+
WritableMap mapE = Arguments.createMap();
|
|
187
|
+
mapE.putString("message", "Fail while copying file to Gallery");
|
|
188
|
+
sendEvent(getReactApplicationContext(), "onError", mapE);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.hideDialog();
|
|
192
|
+
} else {
|
|
193
|
+
WritableMap mapE = Arguments.createMap();
|
|
194
|
+
mapE.putString("message", "Fail to save to Gallery. Please check if you have correct permission");
|
|
195
|
+
sendEvent(getReactApplicationContext(), "onError", mapE);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
WritableMap map = Arguments.createMap();
|
|
200
|
+
map.putString("outputPath", in);
|
|
201
|
+
sendEvent(getReactApplicationContext(), "onFinishTrimming", map);
|
|
156
202
|
}
|
|
157
|
-
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Override public void onError() {
|
|
207
|
+
WritableMap map = Arguments.createMap();
|
|
208
|
+
map.putString("message", "Error when trimming, please try again");
|
|
209
|
+
sendEvent(getReactApplicationContext(), "onError", map);
|
|
158
210
|
this.hideDialog();
|
|
159
211
|
}
|
|
160
212
|
|
|
@@ -164,6 +216,12 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
164
216
|
}
|
|
165
217
|
|
|
166
218
|
private void hideDialog() {
|
|
219
|
+
if (mProgressDialog != null) {
|
|
220
|
+
if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
|
|
221
|
+
mProgressBar = null;
|
|
222
|
+
mProgressDialog = null;
|
|
223
|
+
}
|
|
224
|
+
|
|
167
225
|
if (alertDialog != null) {
|
|
168
226
|
if(alertDialog.isShowing()) {
|
|
169
227
|
alertDialog.dismiss();
|
|
@@ -172,12 +230,45 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
172
230
|
}
|
|
173
231
|
}
|
|
174
232
|
|
|
175
|
-
private
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
233
|
+
private void buildDialog() {
|
|
234
|
+
Activity activity = getReactApplicationContext().getCurrentActivity();
|
|
235
|
+
// Create the parent layout for the dialog
|
|
236
|
+
LinearLayout layout = new LinearLayout(activity);
|
|
237
|
+
layout.setLayoutParams(new ViewGroup.LayoutParams(
|
|
238
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
239
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
240
|
+
));
|
|
241
|
+
layout.setOrientation(LinearLayout.VERTICAL);
|
|
242
|
+
layout.setGravity(Gravity.CENTER_HORIZONTAL);
|
|
243
|
+
layout.setPadding(16, 32, 16, 32);
|
|
244
|
+
|
|
245
|
+
// Create and add the TextView
|
|
246
|
+
TextView textView = new TextView(activity);
|
|
247
|
+
textView.setLayoutParams(new ViewGroup.LayoutParams(
|
|
248
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
249
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
250
|
+
));
|
|
251
|
+
textView.setText(getReactApplicationContext().getResources().getString(R.string.trimming));
|
|
252
|
+
textView.setTextSize(18);
|
|
253
|
+
layout.addView(textView);
|
|
254
|
+
|
|
255
|
+
// Create and add the ProgressBar
|
|
256
|
+
mProgressBar = new ProgressBar(activity, null, android.R.attr.progressBarStyleHorizontal);
|
|
257
|
+
mProgressBar.setLayoutParams(new ViewGroup.LayoutParams(
|
|
258
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
259
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
260
|
+
));
|
|
261
|
+
mProgressBar.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#2196F3")));
|
|
262
|
+
layout.addView(mProgressBar);
|
|
263
|
+
|
|
264
|
+
// Create the AlertDialog
|
|
265
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
266
|
+
builder.setCancelable(false);
|
|
267
|
+
builder.setView(layout);
|
|
268
|
+
|
|
269
|
+
// Show the dialog
|
|
270
|
+
mProgressDialog = builder.create();
|
|
271
|
+
mProgressDialog.show();
|
|
181
272
|
}
|
|
182
273
|
|
|
183
274
|
@ReactMethod
|
|
@@ -202,6 +293,7 @@ public class VideoTrimModule extends ReactContextBaseJavaModule implements Video
|
|
|
202
293
|
}
|
|
203
294
|
}
|
|
204
295
|
|
|
296
|
+
|
|
205
297
|
public boolean _isValidVideo(String filePath) {
|
|
206
298
|
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
207
299
|
|
|
@@ -202,27 +202,19 @@ public class StorageUtil {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
public static void saveVideoToGallery(ReactApplicationContext context, String videoFilePath) throws IOException {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} catch (IOException e) {
|
|
219
|
-
throw new RuntimeException(e);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
throw new RuntimeException();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
205
|
+
File videoFile = new File(videoFilePath);
|
|
206
|
+
|
|
207
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
208
|
+
// For Android 10 and higher (API >= 29)
|
|
209
|
+
saveVideoUsingMediaStore(context, videoFile);
|
|
210
|
+
} else {
|
|
211
|
+
// For Android 9 and below (API < 29)
|
|
212
|
+
try {
|
|
213
|
+
saveVideoUsingTraditionalStorage(context, videoFile);
|
|
214
|
+
} catch (IOException e) {
|
|
215
|
+
throw new RuntimeException(e);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
226
218
|
}
|
|
227
219
|
|
|
228
220
|
// Save video using MediaStore for API level >= 29
|
|
@@ -4,7 +4,16 @@ import android.content.Context;
|
|
|
4
4
|
import android.graphics.Bitmap;
|
|
5
5
|
import android.media.MediaMetadataRetriever;
|
|
6
6
|
import android.net.Uri;
|
|
7
|
+
import android.util.Log;
|
|
7
8
|
|
|
9
|
+
import com.arthenica.ffmpegkit.FFmpegKit;
|
|
10
|
+
import com.arthenica.ffmpegkit.FFmpegSession;
|
|
11
|
+
import com.arthenica.ffmpegkit.FFmpegSessionCompleteCallback;
|
|
12
|
+
import com.arthenica.ffmpegkit.LogCallback;
|
|
13
|
+
import com.arthenica.ffmpegkit.ReturnCode;
|
|
14
|
+
import com.arthenica.ffmpegkit.SessionState;
|
|
15
|
+
import com.arthenica.ffmpegkit.Statistics;
|
|
16
|
+
import com.arthenica.ffmpegkit.StatisticsCallback;
|
|
8
17
|
import com.videotrim.interfaces.VideoTrimListener;
|
|
9
18
|
|
|
10
19
|
import java.text.SimpleDateFormat;
|
|
@@ -15,8 +24,6 @@ import iknow.android.utils.DeviceUtil;
|
|
|
15
24
|
import iknow.android.utils.UnitConverter;
|
|
16
25
|
import iknow.android.utils.callback.SingleCallback;
|
|
17
26
|
import iknow.android.utils.thread.BackgroundExecutor;
|
|
18
|
-
import nl.bravobit.ffmpeg.ExecuteBinaryResponseHandler;
|
|
19
|
-
import nl.bravobit.ffmpeg.FFmpeg;
|
|
20
27
|
|
|
21
28
|
public class VideoTrimmerUtil {
|
|
22
29
|
|
|
@@ -34,28 +41,37 @@ public class VideoTrimmerUtil {
|
|
|
34
41
|
public static final int THUMB_HEIGHT = UnitConverter.dpToPx(50); // x2 for better resolution
|
|
35
42
|
private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
|
|
36
43
|
|
|
37
|
-
public static void trim(Context context, String inputFile, String outputFile, long startMs, long endMs, final VideoTrimListener callback) {
|
|
44
|
+
public static void trim(Context context, String inputFile, String outputFile, int videoDuration, long startMs, long endMs, final VideoTrimListener callback) {
|
|
38
45
|
final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
|
|
39
46
|
final String outputName = "trimmedVideo_" + timeStamp + ".mp4";
|
|
40
47
|
outputFile = outputFile + "/" + outputName;
|
|
41
48
|
|
|
42
49
|
String cmd = "-i " + inputFile + " -ss " + startMs + "ms" + " -to " + endMs + "ms -c copy " + outputFile;
|
|
43
50
|
String[] command = cmd.split(" ");
|
|
44
|
-
|
|
45
|
-
final String tempOutFile = outputFile;
|
|
46
|
-
FFmpeg.getInstance(context).execute(command, new ExecuteBinaryResponseHandler() {
|
|
51
|
+
final String tempOutFile = outputFile;
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
callback.onStartTrim();
|
|
54
|
+
FFmpegKit.executeAsync(cmd, session -> {
|
|
55
|
+
SessionState state = session.getState();
|
|
56
|
+
ReturnCode returnCode = session.getReturnCode();
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
Log.d(TAG, String.format("FFmpeg process exited with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace()));
|
|
59
|
+
|
|
60
|
+
if (state.equals(SessionState.COMPLETED)) {
|
|
61
|
+
callback.onFinishTrim(tempOutFile);
|
|
62
|
+
} else {
|
|
63
|
+
callback.onError();
|
|
64
|
+
}
|
|
65
|
+
}, log -> {
|
|
66
|
+
|
|
67
|
+
}, statistics -> {
|
|
68
|
+
int timeInMilliseconds = statistics.getTime();
|
|
69
|
+
if (timeInMilliseconds > 0) {
|
|
70
|
+
int completePercentage =
|
|
71
|
+
(timeInMilliseconds * 100) / videoDuration;
|
|
72
|
+
callback.onTrimmingProgress(Math.min(Math.max(completePercentage, 0), 100));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
public static void shootVideoThumbInBackground(final Context context, final Uri videoUri, final int totalThumbsCount, final long startPosition,
|
|
@@ -277,6 +277,7 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
|
277
277
|
VideoTrimmerUtil.trim(mContext,
|
|
278
278
|
mSourceUri.getPath(),
|
|
279
279
|
StorageUtil.getCacheDir(),
|
|
280
|
+
mDuration,
|
|
280
281
|
mLeftProgressPos,
|
|
281
282
|
mRightProgressPos,
|
|
282
283
|
mOnTrimVideoListener);
|