react-native-video-trim 0.0.1 → 1.0.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/LICENSE +20 -0
- package/README.md +206 -0
- package/android/build.gradle +105 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/AndroidManifestDeprecated.xml +3 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +223 -0
- package/android/src/main/java/com/videotrim/VideoTrimPackage.java +28 -0
- package/android/src/main/java/com/videotrim/adapters/VideoTrimmerAdapter.java +60 -0
- package/android/src/main/java/com/videotrim/interfaces/IVideoTrimmerView.java +5 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +7 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +285 -0
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +93 -0
- package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +534 -0
- package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +33 -0
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +443 -0
- package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +48 -0
- package/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
- package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
- package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
- package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
- package/android/src/main/res/layout/video_thumb_item_layout.xml +16 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +148 -0
- package/android/src/main/res/values/colors.xml +17 -0
- package/android/src/main/res/values/strings.xml +14 -0
- package/ios/VideoTrim-Bridging-Header.h +2 -0
- package/ios/VideoTrim.mm +10 -0
- package/ios/VideoTrim.swift +170 -0
- package/ios/VideoTrim.xcodeproj/project.pbxproj +283 -0
- package/ios/VideoTrim.xcodeproj/project.xcworkspace/contents.xcworkspacedata +4 -0
- package/lib/commonjs/index.js +32 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +25 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +158 -7
- package/react-native-video-trim.podspec +41 -0
- package/src/index.tsx +35 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
package com.videotrim.utils;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.annotation.SuppressLint;
|
|
5
|
+
import android.content.ContentValues;
|
|
6
|
+
import android.content.Context;
|
|
7
|
+
import android.media.MediaScannerConnection;
|
|
8
|
+
import android.net.Uri;
|
|
9
|
+
import android.os.Build;
|
|
10
|
+
import android.os.Environment;
|
|
11
|
+
import android.provider.MediaStore;
|
|
12
|
+
import android.text.TextUtils;
|
|
13
|
+
import android.util.Log;
|
|
14
|
+
|
|
15
|
+
import androidx.fragment.app.FragmentActivity;
|
|
16
|
+
|
|
17
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
18
|
+
import com.permissionx.guolindev.PermissionX;
|
|
19
|
+
|
|
20
|
+
import java.io.File;
|
|
21
|
+
import java.io.FileInputStream;
|
|
22
|
+
import java.io.FileOutputStream;
|
|
23
|
+
import java.io.IOException;
|
|
24
|
+
import java.io.InputStream;
|
|
25
|
+
import java.io.OutputStream;
|
|
26
|
+
import java.util.Locale;
|
|
27
|
+
|
|
28
|
+
import iknow.android.utils.BaseUtils;
|
|
29
|
+
import iknow.android.utils.BuildConfig;
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@SuppressWarnings({ "ResultOfMethodCallIgnored", "FieldCanBeLocal" })
|
|
33
|
+
public class StorageUtil {
|
|
34
|
+
|
|
35
|
+
private static final String TAG = "StorageUtil";
|
|
36
|
+
private static String APP_DATA_PATH = "/Android/data/" + BuildConfig.APPLICATION_ID;
|
|
37
|
+
|
|
38
|
+
private static String sDataDir;
|
|
39
|
+
private static String sCacheDir;
|
|
40
|
+
|
|
41
|
+
public static String getAppDataDir() {
|
|
42
|
+
if (TextUtils.isEmpty(sDataDir)) {
|
|
43
|
+
try {
|
|
44
|
+
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
|
45
|
+
sDataDir = Environment.getExternalStorageDirectory().getPath() + APP_DATA_PATH;
|
|
46
|
+
if (TextUtils.isEmpty(sDataDir)) {
|
|
47
|
+
sDataDir = BaseUtils.getContext().getFilesDir().getAbsolutePath();
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
sDataDir = BaseUtils.getContext().getFilesDir().getAbsolutePath();
|
|
51
|
+
}
|
|
52
|
+
} catch (Throwable e) {
|
|
53
|
+
e.printStackTrace();
|
|
54
|
+
sDataDir = BaseUtils.getContext().getFilesDir().getAbsolutePath();
|
|
55
|
+
}
|
|
56
|
+
File file = new File(sDataDir);
|
|
57
|
+
if (!file.exists()) {//判断文件目录是否存在
|
|
58
|
+
file.mkdirs();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return sDataDir;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static String getCacheDir() {
|
|
65
|
+
if (TextUtils.isEmpty(sCacheDir)) {
|
|
66
|
+
File file = null;
|
|
67
|
+
Context context = BaseUtils.getContext();
|
|
68
|
+
try {
|
|
69
|
+
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
|
70
|
+
file = context.getExternalCacheDir();
|
|
71
|
+
if (file == null || !file.exists()) {
|
|
72
|
+
file = getExternalCacheDirManual(context);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (file == null) {
|
|
76
|
+
file = context.getCacheDir();
|
|
77
|
+
if (file == null || !file.exists()) {
|
|
78
|
+
file = getCacheDirManual(context);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
Log.w(TAG, "cache dir = " + file.getAbsolutePath());
|
|
82
|
+
sCacheDir = file.getAbsolutePath();
|
|
83
|
+
} catch (Throwable ignored) {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return sCacheDir;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static File getExternalCacheDirManual(Context context) {
|
|
90
|
+
File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data");
|
|
91
|
+
File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache");
|
|
92
|
+
if (!appCacheDir.exists()) {
|
|
93
|
+
if (!appCacheDir.mkdirs()) {//
|
|
94
|
+
Log.w(TAG, "Unable to create external cache directory");
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
new File(appCacheDir, ".nomedia").createNewFile();
|
|
99
|
+
} catch (IOException e) {
|
|
100
|
+
Log.i(TAG, "Can't create \".nomedia\" file in application external cache directory");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return appCacheDir;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@SuppressLint("SdCardPath")
|
|
107
|
+
private static File getCacheDirManual(Context context) {
|
|
108
|
+
String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache";
|
|
109
|
+
return new File(cacheDirPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public static boolean delFiles(String path) {
|
|
113
|
+
File cacheFile = new File(path);
|
|
114
|
+
if (!cacheFile.exists()) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
File[] files = cacheFile.listFiles();
|
|
118
|
+
for (int i = 0; i < files.length; i++) {
|
|
119
|
+
// 是文件则直接删除
|
|
120
|
+
if (files[i].exists() && files[i].isFile()) {
|
|
121
|
+
files[i].delete();
|
|
122
|
+
} else if (files[i].exists() && files[i].isDirectory()) {
|
|
123
|
+
// 递归删除文件
|
|
124
|
+
delFiles(files[i].getAbsolutePath());
|
|
125
|
+
// 删除完目录下面的所有文件后再删除该文件夹
|
|
126
|
+
files[i].delete();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public static long sizeOfDirectory(File dir) {
|
|
134
|
+
if (dir.exists()) {
|
|
135
|
+
long result = 0;
|
|
136
|
+
File[] fileList = dir.listFiles();
|
|
137
|
+
for (int i = 0; i < fileList.length; i++) {
|
|
138
|
+
// Recursive call if it's a directory
|
|
139
|
+
if (fileList[i].isDirectory()) {
|
|
140
|
+
result += sizeOfDirectory(fileList[i]);
|
|
141
|
+
} else {
|
|
142
|
+
// Sum the file size in bytes
|
|
143
|
+
result += fileList[i].length();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result; // return the file size
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param length 长度 byte为单位
|
|
153
|
+
* 将文件大小转换为KB,MB格式
|
|
154
|
+
*/
|
|
155
|
+
public static String getFileSize(long length) {
|
|
156
|
+
int MB = 1024 * 1024;
|
|
157
|
+
if (length < MB) {
|
|
158
|
+
double resultKB = length * 1.0 / 1024;
|
|
159
|
+
return String.format(Locale.getDefault(), "%.1f", resultKB) + "Kb";
|
|
160
|
+
}
|
|
161
|
+
double resultMB = length * 1.0 / MB;
|
|
162
|
+
return String.format(Locale.getDefault(), "%.1f", resultMB) + "Mb";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public static boolean isFileExist(String path) {
|
|
166
|
+
if (TextUtils.isEmpty(path)) return false;
|
|
167
|
+
File file = new File(path);
|
|
168
|
+
return file.exists();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param path 路径
|
|
173
|
+
* @return 是否删除成功
|
|
174
|
+
*/
|
|
175
|
+
public static boolean deleteFile(String path) {
|
|
176
|
+
if (TextUtils.isEmpty(path)) return true;
|
|
177
|
+
return deleteFile(new File(path));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @return 是否删除成功
|
|
182
|
+
*/
|
|
183
|
+
public static boolean deleteFile(File file) {
|
|
184
|
+
if (file == null || !file.exists()) return true;
|
|
185
|
+
|
|
186
|
+
if (file.isFile()) {
|
|
187
|
+
return file.delete();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!file.isDirectory()) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (File f : file.listFiles()) {
|
|
195
|
+
if (f.isFile()) {
|
|
196
|
+
f.delete();
|
|
197
|
+
} else if (f.isDirectory()) {
|
|
198
|
+
deleteFile(f);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return file.delete();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public static void saveVideoToGallery(ReactApplicationContext context, String videoFilePath) throws IOException {
|
|
205
|
+
PermissionX.init((FragmentActivity) context.getCurrentActivity())
|
|
206
|
+
.permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
207
|
+
.request((allGranted, grantedList, deniedList) -> {
|
|
208
|
+
if (allGranted) {
|
|
209
|
+
File videoFile = new File(videoFilePath);
|
|
210
|
+
|
|
211
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
212
|
+
// For Android 10 and higher (API >= 29)
|
|
213
|
+
saveVideoUsingMediaStore(context, videoFile);
|
|
214
|
+
} else {
|
|
215
|
+
// For Android 9 and below (API < 29)
|
|
216
|
+
try {
|
|
217
|
+
saveVideoUsingTraditionalStorage(context, videoFile);
|
|
218
|
+
} catch (IOException e) {
|
|
219
|
+
throw new RuntimeException(e);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
throw new RuntimeException();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Save video using MediaStore for API level >= 29
|
|
229
|
+
private static void saveVideoUsingMediaStore(Context context, File videoFile) {
|
|
230
|
+
ContentValues values = new ContentValues();
|
|
231
|
+
values.put(MediaStore.Video.Media.TITLE, "My Video Title");
|
|
232
|
+
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
|
|
233
|
+
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
|
|
234
|
+
Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
|
|
235
|
+
|
|
236
|
+
if (uri != null) {
|
|
237
|
+
try {
|
|
238
|
+
OutputStream outputStream = context.getContentResolver().openOutputStream(uri);
|
|
239
|
+
if (outputStream != null) {
|
|
240
|
+
// Copy the video file to the output stream
|
|
241
|
+
// Here, you can use the method you have to copy the file contents
|
|
242
|
+
// For example, you can use FileInputStream to read from videoFile and write to outputStream
|
|
243
|
+
|
|
244
|
+
outputStream.close();
|
|
245
|
+
// Notify the media scanner that a new video has been added to the gallery
|
|
246
|
+
MediaScannerConnection.scanFile(context, new String[]{videoFile.getAbsolutePath()}, new String[]{"video/*"}, null);
|
|
247
|
+
}
|
|
248
|
+
} catch (IOException e) {
|
|
249
|
+
e.printStackTrace();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Save video using traditional storage for API level < 29
|
|
255
|
+
private static void saveVideoUsingTraditionalStorage(Context context, File videoFile) throws IOException {
|
|
256
|
+
File galleryDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
|
257
|
+
File destinationFile = new File(galleryDirectory, videoFile.getName());
|
|
258
|
+
copyFile(videoFile, destinationFile);
|
|
259
|
+
MediaScannerConnection.scanFile(context, new String[]{destinationFile.getAbsolutePath()}, new String[]{"video/*"}, null);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private static void copyFile(File sourceFile, File destFile) throws IOException {
|
|
263
|
+
InputStream inputStream = null;
|
|
264
|
+
OutputStream outputStream = null;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
inputStream = new FileInputStream(sourceFile);
|
|
268
|
+
outputStream = new FileOutputStream(destFile);
|
|
269
|
+
|
|
270
|
+
byte[] buffer = new byte[1024];
|
|
271
|
+
int length;
|
|
272
|
+
while ((length = inputStream.read(buffer)) > 0) {
|
|
273
|
+
outputStream.write(buffer, 0, length);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
} finally {
|
|
277
|
+
if (inputStream != null) {
|
|
278
|
+
inputStream.close();
|
|
279
|
+
}
|
|
280
|
+
if (outputStream != null) {
|
|
281
|
+
outputStream.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package com.videotrim.utils;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.media.MediaMetadataRetriever;
|
|
6
|
+
import android.net.Uri;
|
|
7
|
+
|
|
8
|
+
import com.videotrim.interfaces.VideoTrimListener;
|
|
9
|
+
|
|
10
|
+
import java.text.SimpleDateFormat;
|
|
11
|
+
import java.util.Date;
|
|
12
|
+
import java.util.Locale;
|
|
13
|
+
|
|
14
|
+
import iknow.android.utils.DeviceUtil;
|
|
15
|
+
import iknow.android.utils.UnitConverter;
|
|
16
|
+
import iknow.android.utils.callback.SingleCallback;
|
|
17
|
+
import iknow.android.utils.thread.BackgroundExecutor;
|
|
18
|
+
import nl.bravobit.ffmpeg.ExecuteBinaryResponseHandler;
|
|
19
|
+
import nl.bravobit.ffmpeg.FFmpeg;
|
|
20
|
+
|
|
21
|
+
public class VideoTrimmerUtil {
|
|
22
|
+
|
|
23
|
+
private static final String TAG = VideoTrimmerUtil.class.getSimpleName();
|
|
24
|
+
public static final long MIN_SHOOT_DURATION = 3000L;// min 3 seconds for trimming
|
|
25
|
+
// public static final int VIDEO_MAX_TIME = 10;// max 10 seconds for trimming
|
|
26
|
+
// public static final long MAX_SHOOT_DURATION = VIDEO_MAX_TIME * 1000L;
|
|
27
|
+
public static long maxShootDuration = 10 * 1000L;
|
|
28
|
+
public static final int MAX_COUNT_RANGE = 10; // how many images in the highlight range of seek bar
|
|
29
|
+
public static final int SCREEN_WIDTH_FULL = DeviceUtil.getDeviceWidth();
|
|
30
|
+
public static final int RECYCLER_VIEW_PADDING = UnitConverter.dpToPx(35);
|
|
31
|
+
public static final int VIDEO_FRAMES_WIDTH = SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
|
|
32
|
+
// public static final int THUMB_WIDTH = (SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2) / VIDEO_MAX_TIME;
|
|
33
|
+
public static int mThumbWidth = 0; // make it automatic
|
|
34
|
+
public static final int THUMB_HEIGHT = UnitConverter.dpToPx(50); // x2 for better resolution
|
|
35
|
+
private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
|
|
36
|
+
|
|
37
|
+
public static void trim(Context context, String inputFile, String outputFile, long startMs, long endMs, final VideoTrimListener callback) {
|
|
38
|
+
final String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
|
|
39
|
+
final String outputName = "trimmedVideo_" + timeStamp + ".mp4";
|
|
40
|
+
outputFile = outputFile + "/" + outputName;
|
|
41
|
+
|
|
42
|
+
String cmd = "-i " + inputFile + " -ss " + startMs + "ms" + " -to " + endMs + "ms -c copy " + outputFile;
|
|
43
|
+
String[] command = cmd.split(" ");
|
|
44
|
+
try {
|
|
45
|
+
final String tempOutFile = outputFile;
|
|
46
|
+
FFmpeg.getInstance(context).execute(command, new ExecuteBinaryResponseHandler() {
|
|
47
|
+
|
|
48
|
+
@Override public void onSuccess(String s) {
|
|
49
|
+
callback.onFinishTrim(tempOutFile);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Override public void onStart() {
|
|
53
|
+
callback.onStartTrim();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
} catch (Exception e) {
|
|
57
|
+
e.printStackTrace();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public static void shootVideoThumbInBackground(final Context context, final Uri videoUri, final int totalThumbsCount, final long startPosition,
|
|
62
|
+
final long endPosition, final SingleCallback<Bitmap, Integer> callback) {
|
|
63
|
+
BackgroundExecutor.execute(new BackgroundExecutor.Task("", 0L, "") {
|
|
64
|
+
@Override public void execute() {
|
|
65
|
+
try {
|
|
66
|
+
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
|
|
67
|
+
mediaMetadataRetriever.setDataSource(context, videoUri);
|
|
68
|
+
// Retrieve media data use microsecond
|
|
69
|
+
long interval = (endPosition - startPosition) / (totalThumbsCount - 1);
|
|
70
|
+
for (long i = 0; i < totalThumbsCount; ++i) {
|
|
71
|
+
long frameTime = startPosition + interval * i;
|
|
72
|
+
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(frameTime * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
|
73
|
+
if(bitmap == null) continue;
|
|
74
|
+
try {
|
|
75
|
+
if (mThumbWidth <= 0) { // only take first item
|
|
76
|
+
int width = THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
|
|
77
|
+
mThumbWidth = width;
|
|
78
|
+
}
|
|
79
|
+
bitmap = Bitmap.createScaledBitmap(bitmap, mThumbWidth * THUMB_RESOLUTION_RES, THUMB_HEIGHT * THUMB_RESOLUTION_RES, false);
|
|
80
|
+
} catch (final Throwable t) {
|
|
81
|
+
t.printStackTrace();
|
|
82
|
+
}
|
|
83
|
+
callback.onSingleCallback(bitmap, (int) interval);
|
|
84
|
+
}
|
|
85
|
+
mediaMetadataRetriever.release();
|
|
86
|
+
} catch (final Throwable e) {
|
|
87
|
+
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|