react-native-video-trim 6.1.0 → 6.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/android/src/main/java/com/videotrim/enums/{ErrorCode.java → ErrorCode.kt} +2 -2
- package/android/src/main/java/com/videotrim/interfaces/IVideoTrimmerView.kt +5 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.kt +16 -0
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.kt +84 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +129 -0
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +163 -0
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +1170 -0
- package/ios/VideoTrimmer.swift +102 -3
- package/ios/VideoTrimmerViewController.swift +18 -0
- package/package.json +2 -2
- package/android/src/main/java/com/videotrim/interfaces/IVideoTrimmerView.java +0 -5
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +0 -19
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +0 -92
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +0 -147
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +0 -171
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +0 -1248
package/ios/VideoTrimmer.swift
CHANGED
|
@@ -26,6 +26,11 @@ import AVFoundation
|
|
|
26
26
|
static let progressChanged = UIControl.Event(rawValue: 0b00010000 << 24)
|
|
27
27
|
static let didEndScrubbing = UIControl.Event(rawValue: 0b00100000 << 24)
|
|
28
28
|
|
|
29
|
+
// events for dragging the entire selected range
|
|
30
|
+
static let didBeginDraggingRange = UIControl.Event(rawValue: 0b01000000 << 24)
|
|
31
|
+
static let rangeDragChanged = UIControl.Event(rawValue: 0b10000000 << 24)
|
|
32
|
+
static let didEndDraggingRange = UIControl.Event(rawValue: 1 << 25)
|
|
33
|
+
|
|
29
34
|
private struct Thumbnail {
|
|
30
35
|
let uuid = UUID()
|
|
31
36
|
let imageView: UIImageView
|
|
@@ -249,6 +254,12 @@ import AVFoundation
|
|
|
249
254
|
private (set) var trailingGestureRecognizer: UILongPressGestureRecognizer!
|
|
250
255
|
private (set) var progressGestureRecognizer: UILongPressGestureRecognizer!
|
|
251
256
|
private (set) var thumbnailInteractionGestureRecognizer: UILongPressGestureRecognizer!
|
|
257
|
+
private (set) var rangeDragGestureRecognizer: UILongPressGestureRecognizer!
|
|
258
|
+
|
|
259
|
+
// range drag state
|
|
260
|
+
private(set) var isDraggingRange = false
|
|
261
|
+
private var rangeDragInitialRange: CMTimeRange = .zero
|
|
262
|
+
private var rangeDragInitialLocationX: CGFloat = 0
|
|
252
263
|
|
|
253
264
|
// private stuff
|
|
254
265
|
private var grabberOffset = CGFloat(0)
|
|
@@ -369,11 +380,18 @@ import AVFoundation
|
|
|
369
380
|
progressGestureRecognizer.require(toFail: trailingGestureRecognizer)
|
|
370
381
|
progressIndicatorControl.addGestureRecognizer(progressGestureRecognizer)
|
|
371
382
|
|
|
383
|
+
// Range drag: platform-default long press (0.5s hold, 10pt allowable movement)
|
|
384
|
+
rangeDragGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(rangeDragPanned(_:)))
|
|
385
|
+
rangeDragGestureRecognizer.require(toFail: leadingGestureRecognizer)
|
|
386
|
+
rangeDragGestureRecognizer.require(toFail: trailingGestureRecognizer)
|
|
387
|
+
thumbView.addGestureRecognizer(rangeDragGestureRecognizer)
|
|
388
|
+
|
|
372
389
|
thumbnailInteractionGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(thumbnailPanned(_:)))
|
|
373
390
|
thumbnailInteractionGestureRecognizer.allowableMovement = CGFloat.greatestFiniteMagnitude
|
|
374
391
|
thumbnailInteractionGestureRecognizer.minimumPressDuration = 0
|
|
375
392
|
thumbnailInteractionGestureRecognizer.require(toFail: leadingGestureRecognizer)
|
|
376
393
|
thumbnailInteractionGestureRecognizer.require(toFail: trailingGestureRecognizer)
|
|
394
|
+
thumbnailInteractionGestureRecognizer.require(toFail: rangeDragGestureRecognizer)
|
|
377
395
|
thumbView.addGestureRecognizer(thumbnailInteractionGestureRecognizer)
|
|
378
396
|
}
|
|
379
397
|
|
|
@@ -583,9 +601,10 @@ import AVFoundation
|
|
|
583
601
|
setNeedsLayout()
|
|
584
602
|
|
|
585
603
|
case .hiddenOnlyWhenTrimming:
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
604
|
+
let shouldShow = trimmingState == .none && !isDraggingRange
|
|
605
|
+
progressIndicator.alpha = (shouldShow ? 1 : 0)
|
|
606
|
+
progressIndicatorControl.isUserInteractionEnabled = shouldShow
|
|
607
|
+
if shouldShow {
|
|
589
608
|
setNeedsLayout()
|
|
590
609
|
if UIView.inheritedAnimationDuration > 0 {
|
|
591
610
|
UIView.performWithoutAnimation {
|
|
@@ -780,6 +799,86 @@ import AVFoundation
|
|
|
780
799
|
}
|
|
781
800
|
}
|
|
782
801
|
|
|
802
|
+
|
|
803
|
+
@objc private func rangeDragPanned(_ sender: UILongPressGestureRecognizer) {
|
|
804
|
+
switch sender.state {
|
|
805
|
+
case .began:
|
|
806
|
+
isDraggingRange = true
|
|
807
|
+
rangeDragInitialRange = selectedRange
|
|
808
|
+
rangeDragInitialLocationX = sender.location(in: self).x
|
|
809
|
+
didClampWhilePanning = false
|
|
810
|
+
|
|
811
|
+
if enableHapticFeedback {
|
|
812
|
+
UISelectionFeedbackGenerator().selectionChanged()
|
|
813
|
+
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
814
|
+
impactFeedbackGenerator?.prepare()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
818
|
+
self.updateProgressIndicator()
|
|
819
|
+
})
|
|
820
|
+
sendActions(for: Self.didBeginDraggingRange)
|
|
821
|
+
|
|
822
|
+
case .changed:
|
|
823
|
+
let currentX = sender.location(in: self).x
|
|
824
|
+
let deltaX = currentX - rangeDragInitialLocationX
|
|
825
|
+
let inset = thumbView.chevronWidth + horizontalInset
|
|
826
|
+
let availableWidth = bounds.width - inset * 2
|
|
827
|
+
let visibleDurationInSeconds = CGFloat(visibleRange.duration.seconds)
|
|
828
|
+
guard availableWidth > 0 && visibleDurationInSeconds > 0 else { return }
|
|
829
|
+
|
|
830
|
+
let deltaTime = CMTime(
|
|
831
|
+
seconds: Double(deltaX / availableWidth) * Double(visibleDurationInSeconds),
|
|
832
|
+
preferredTimescale: 600
|
|
833
|
+
)
|
|
834
|
+
let duration = rangeDragInitialRange.duration
|
|
835
|
+
var newStart = CMTimeAdd(rangeDragInitialRange.start, deltaTime)
|
|
836
|
+
var newEnd = CMTimeAdd(newStart, duration)
|
|
837
|
+
|
|
838
|
+
var didClamp = false
|
|
839
|
+
if CMTimeCompare(newStart, range.start) == -1 {
|
|
840
|
+
newStart = range.start
|
|
841
|
+
newEnd = CMTimeAdd(newStart, duration)
|
|
842
|
+
didClamp = true
|
|
843
|
+
}
|
|
844
|
+
if CMTimeCompare(newEnd, range.end) == 1 {
|
|
845
|
+
newEnd = range.end
|
|
846
|
+
newStart = CMTimeSubtract(newEnd, duration)
|
|
847
|
+
didClamp = true
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if didClamp && !didClampWhilePanning {
|
|
851
|
+
impactFeedbackGenerator?.impactOccurred()
|
|
852
|
+
}
|
|
853
|
+
didClampWhilePanning = didClamp
|
|
854
|
+
|
|
855
|
+
selectedRange = CMTimeRange(start: newStart, end: newEnd)
|
|
856
|
+
setNeedsLayout()
|
|
857
|
+
sendActions(for: Self.rangeDragChanged)
|
|
858
|
+
|
|
859
|
+
case .ended:
|
|
860
|
+
isDraggingRange = false
|
|
861
|
+
impactFeedbackGenerator = nil
|
|
862
|
+
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
863
|
+
self.updateProgressIndicator()
|
|
864
|
+
})
|
|
865
|
+
sendActions(for: Self.didEndDraggingRange)
|
|
866
|
+
|
|
867
|
+
case .cancelled:
|
|
868
|
+
isDraggingRange = false
|
|
869
|
+
impactFeedbackGenerator = nil
|
|
870
|
+
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
871
|
+
self.updateProgressIndicator()
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
case .possible, .failed:
|
|
875
|
+
break
|
|
876
|
+
|
|
877
|
+
@unknown default:
|
|
878
|
+
break
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
783
882
|
// MARK: - UIView
|
|
784
883
|
|
|
785
884
|
override var intrinsicContentSize: CGSize {
|
|
@@ -131,6 +131,20 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
131
131
|
handleTrimmingEnd(false)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
@objc private func didBeginDraggingRange(_ sender: VideoTrimmer) {
|
|
135
|
+
handleBeforeProgressChange()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@objc private func rangeDragChanged(_ sender: VideoTrimmer) {
|
|
139
|
+
handleProgressChanged(time: trimmer.selectedRange.start)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@objc private func didEndDraggingRange(_ sender: VideoTrimmer) {
|
|
143
|
+
self.trimmer.progress = trimmer.selectedRange.start
|
|
144
|
+
updateLabels()
|
|
145
|
+
seek(to: trimmer.progress)
|
|
146
|
+
}
|
|
147
|
+
|
|
134
148
|
@objc private func didBeginScrubbing(_ sender: VideoTrimmer) {
|
|
135
149
|
handleBeforeProgressChange()
|
|
136
150
|
}
|
|
@@ -354,6 +368,10 @@ class VideoTrimmerViewController: UIViewController {
|
|
|
354
368
|
trimmer.addTarget(self, action: #selector(didBeginTrimmingFromEnd(_:)), for: VideoTrimmer.didBeginTrimmingFromEnd)
|
|
355
369
|
trimmer.addTarget(self, action: #selector(trailingGrabberChanged(_:)), for: VideoTrimmer.trailingGrabberChanged)
|
|
356
370
|
trimmer.addTarget(self, action: #selector(didEndTrimmingFromEnd(_:)), for: VideoTrimmer.didEndTrimmingFromEnd)
|
|
371
|
+
|
|
372
|
+
trimmer.addTarget(self, action: #selector(didBeginDraggingRange(_:)), for: VideoTrimmer.didBeginDraggingRange)
|
|
373
|
+
trimmer.addTarget(self, action: #selector(rangeDragChanged(_:)), for: VideoTrimmer.rangeDragChanged)
|
|
374
|
+
trimmer.addTarget(self, action: #selector(didEndDraggingRange(_:)), for: VideoTrimmer.didEndDraggingRange)
|
|
357
375
|
trimmer.alpha = 0
|
|
358
376
|
view.addSubview(trimmer)
|
|
359
377
|
trimmer.translatesAutoresizingMaskIntoConstraints = false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-video-trim",
|
|
3
|
-
"version": "6.1
|
|
3
|
+
"version": "6.2.1",
|
|
4
4
|
"description": "Video trimmer for your React Native app",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"workspaces": [
|
|
92
92
|
"example"
|
|
93
93
|
],
|
|
94
|
-
"packageManager": "yarn@
|
|
94
|
+
"packageManager": "yarn@4.13.0",
|
|
95
95
|
"jest": {
|
|
96
96
|
"preset": "react-native",
|
|
97
97
|
"modulePathIgnorePatterns": [
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
package com.videotrim.interfaces;
|
|
2
|
-
|
|
3
|
-
import com.facebook.react.bridge.ReadableMap;
|
|
4
|
-
import com.facebook.react.bridge.WritableMap;
|
|
5
|
-
import com.videotrim.enums.ErrorCode;
|
|
6
|
-
|
|
7
|
-
import java.util.Map;
|
|
8
|
-
|
|
9
|
-
public interface VideoTrimListener {
|
|
10
|
-
void onLoad(int duration);
|
|
11
|
-
void onTrimmingProgress(int percentage);
|
|
12
|
-
void onFinishTrim(String url, long startMs, long endMs, int videoDuration);
|
|
13
|
-
void onCancelTrim();
|
|
14
|
-
void onError(String errorMessage, ErrorCode errorCode);
|
|
15
|
-
void onCancel();
|
|
16
|
-
void onSave();
|
|
17
|
-
void onLog(WritableMap log);
|
|
18
|
-
void onStatistics(WritableMap statistics);
|
|
19
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
package com.videotrim.utils;
|
|
2
|
-
|
|
3
|
-
import android.media.MediaMetadataRetriever;
|
|
4
|
-
import android.net.Uri;
|
|
5
|
-
import android.util.Log;
|
|
6
|
-
|
|
7
|
-
import java.io.File;
|
|
8
|
-
import java.io.IOException;
|
|
9
|
-
import java.util.HashMap;
|
|
10
|
-
|
|
11
|
-
public class MediaMetadataUtil {
|
|
12
|
-
|
|
13
|
-
private static final String TAG = "MediaMetadataUtil";
|
|
14
|
-
|
|
15
|
-
// Function to return MediaMetadataRetriever or null
|
|
16
|
-
public static MediaMetadataRetriever getMediaMetadataRetriever(String source) {
|
|
17
|
-
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
18
|
-
try {
|
|
19
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
20
|
-
retriever.setDataSource(source, new HashMap<>());
|
|
21
|
-
} else {
|
|
22
|
-
String filePath = source;
|
|
23
|
-
|
|
24
|
-
// if "source" is not a valid file path, try to parse it as a URI
|
|
25
|
-
if (!StorageUtil.isFileExists(filePath)) {
|
|
26
|
-
Log.e(TAG, "File does not exist, trying to parse as URI: " + source);
|
|
27
|
-
|
|
28
|
-
Uri uri = Uri.parse(source);
|
|
29
|
-
filePath = uri.getPath();
|
|
30
|
-
|
|
31
|
-
if (!StorageUtil.isFileExists(filePath)) {
|
|
32
|
-
Log.e(TAG, "File does not exist at path: " + filePath);
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
retriever.setDataSource(filePath);
|
|
38
|
-
}
|
|
39
|
-
return retriever;
|
|
40
|
-
} catch (Exception e) {
|
|
41
|
-
Log.e(TAG, "Error setting data source", e);
|
|
42
|
-
try {
|
|
43
|
-
retriever.release();
|
|
44
|
-
} catch (Exception ee) {
|
|
45
|
-
Log.e(TAG, "Error releasing retriever", ee);
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public static void checkFileValidity(String urlString, FileValidityCallback callback) {
|
|
52
|
-
new Thread(() -> {
|
|
53
|
-
boolean isValid = false;
|
|
54
|
-
String fileType = "unknown";
|
|
55
|
-
long duration;
|
|
56
|
-
MediaMetadataRetriever retriever = getMediaMetadataRetriever(urlString);
|
|
57
|
-
if (retriever == null) {
|
|
58
|
-
callback.onResult(false, fileType, -1L);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Retrieve the duration
|
|
63
|
-
String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
64
|
-
duration = durationStr == null ? -1L : Long.parseLong(durationStr);
|
|
65
|
-
|
|
66
|
-
// Determine the type
|
|
67
|
-
String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
|
|
68
|
-
if (hasVideo != null && hasVideo.equals("yes")) {
|
|
69
|
-
fileType = "video";
|
|
70
|
-
isValid = true;
|
|
71
|
-
} else {
|
|
72
|
-
String hasAudio = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO);
|
|
73
|
-
if (hasAudio != null && hasAudio.equals("yes")) {
|
|
74
|
-
fileType = "audio";
|
|
75
|
-
isValid = true;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
retriever.release();
|
|
81
|
-
} catch (IOException e) {
|
|
82
|
-
Log.e(TAG, "Error releasing retriever", e);
|
|
83
|
-
}
|
|
84
|
-
callback.onResult(isValid, fileType, isValid ? duration : -1L);
|
|
85
|
-
}).start();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public interface FileValidityCallback {
|
|
89
|
-
void onResult(boolean isValid, String fileType, Long duration);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
package com.videotrim.utils;
|
|
2
|
-
|
|
3
|
-
import android.content.ContentValues;
|
|
4
|
-
import android.content.Context;
|
|
5
|
-
import android.media.MediaScannerConnection;
|
|
6
|
-
import android.net.Uri;
|
|
7
|
-
import android.os.Build;
|
|
8
|
-
import android.os.Environment;
|
|
9
|
-
import android.provider.MediaStore;
|
|
10
|
-
import android.text.TextUtils;
|
|
11
|
-
|
|
12
|
-
import com.facebook.react.bridge.ReactApplicationContext;
|
|
13
|
-
|
|
14
|
-
import java.io.File;
|
|
15
|
-
import java.io.FileInputStream;
|
|
16
|
-
import java.io.FileOutputStream;
|
|
17
|
-
import java.io.IOException;
|
|
18
|
-
import java.io.InputStream;
|
|
19
|
-
import java.io.OutputStream;
|
|
20
|
-
import java.util.ArrayList;
|
|
21
|
-
import java.util.List;
|
|
22
|
-
|
|
23
|
-
public class StorageUtil {
|
|
24
|
-
public static String getOutputPath(Context context, String mOutputExt) {
|
|
25
|
-
long timestamp = System.currentTimeMillis() / 1000;
|
|
26
|
-
File file = new File(context.getFilesDir(), VideoTrimmerUtil.FILE_PREFIX + "_" + timestamp + "." + mOutputExt);
|
|
27
|
-
return file.getAbsolutePath();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public static boolean isFileExists(String filePath) {
|
|
31
|
-
if (TextUtils.isEmpty(filePath)) return false;
|
|
32
|
-
File file = new File(filePath);
|
|
33
|
-
return file.exists();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public static String[] listFiles(Context context) {
|
|
37
|
-
File filesDir = context.getFilesDir();
|
|
38
|
-
File[] files = filesDir.listFiles((dir, name) -> name.startsWith(VideoTrimmerUtil.FILE_PREFIX));
|
|
39
|
-
|
|
40
|
-
List<String> fileUrls = new ArrayList<>();
|
|
41
|
-
if (files != null) {
|
|
42
|
-
for (File file : files) {
|
|
43
|
-
fileUrls.add(file.getAbsolutePath());
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return fileUrls.toArray(new String[0]);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
public static boolean deleteFile(String path) {
|
|
51
|
-
if (TextUtils.isEmpty(path)) return true;
|
|
52
|
-
return deleteFile(new File(path));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
public static boolean deleteFile(File file) {
|
|
56
|
-
if (file == null || !file.exists()) return true;
|
|
57
|
-
|
|
58
|
-
if (file.isFile()) {
|
|
59
|
-
return file.delete();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!file.isDirectory()) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
for (File f : file.listFiles()) {
|
|
67
|
-
if (f.isFile()) {
|
|
68
|
-
f.delete();
|
|
69
|
-
} else if (f.isDirectory()) {
|
|
70
|
-
deleteFile(f);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return file.delete();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
public static void saveVideoToGallery(ReactApplicationContext context, String videoFilePath) throws IOException {
|
|
77
|
-
File videoFile = new File(videoFilePath);
|
|
78
|
-
|
|
79
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
80
|
-
// For Android 10 and higher (API >= 29)
|
|
81
|
-
saveVideoUsingMediaStore(context, videoFile);
|
|
82
|
-
} else {
|
|
83
|
-
// For Android 9 and below (API < 29)
|
|
84
|
-
try {
|
|
85
|
-
saveVideoUsingTraditionalStorage(context, videoFile);
|
|
86
|
-
} catch (IOException e) {
|
|
87
|
-
throw new RuntimeException(e);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Save video using MediaStore for API level >= 29
|
|
93
|
-
private static void saveVideoUsingMediaStore(Context context, File videoFile) {
|
|
94
|
-
ContentValues values = new ContentValues();
|
|
95
|
-
values.put(MediaStore.Video.Media.TITLE, "My Video Title");
|
|
96
|
-
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
|
|
97
|
-
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
|
|
98
|
-
Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
|
|
99
|
-
|
|
100
|
-
if (uri != null) {
|
|
101
|
-
try {
|
|
102
|
-
OutputStream outputStream = context.getContentResolver().openOutputStream(uri);
|
|
103
|
-
copyFile(videoFile, outputStream);
|
|
104
|
-
MediaScannerConnection.scanFile(context, new String[]{uri.toString()}, new String[]{"video/*"}, null);
|
|
105
|
-
} catch (IOException e) {
|
|
106
|
-
e.printStackTrace();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Save video using traditional storage for API level < 29
|
|
112
|
-
private static void saveVideoUsingTraditionalStorage(Context context, File videoFile) throws IOException {
|
|
113
|
-
File galleryDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
|
114
|
-
File destinationFile = new File(galleryDirectory, videoFile.getName());
|
|
115
|
-
copyFile(videoFile, destinationFile);
|
|
116
|
-
MediaScannerConnection.scanFile(context, new String[]{destinationFile.getAbsolutePath()}, new String[]{"video/*"}, null);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private static void copyFile(File sourceFile, OutputStream outputStream) throws IOException {
|
|
120
|
-
InputStream inputStream = new FileInputStream(sourceFile);
|
|
121
|
-
copyFile(inputStream, outputStream);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private static void copyFile(File sourceFile, File destFile) throws IOException {
|
|
125
|
-
InputStream inputStream = new FileInputStream(sourceFile);
|
|
126
|
-
OutputStream outputStream = new FileOutputStream(destFile);
|
|
127
|
-
copyFile(inputStream, outputStream);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private static void copyFile(InputStream inputStream, OutputStream outputStream) throws IOException {
|
|
131
|
-
try {
|
|
132
|
-
byte[] buffer = new byte[1024];
|
|
133
|
-
int length;
|
|
134
|
-
while ((length = inputStream.read(buffer)) > 0) {
|
|
135
|
-
outputStream.write(buffer, 0, length);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
} finally {
|
|
139
|
-
if (inputStream != null) {
|
|
140
|
-
inputStream.close();
|
|
141
|
-
}
|
|
142
|
-
if (outputStream != null) {
|
|
143
|
-
outputStream.close();
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
package com.videotrim.utils;
|
|
2
|
-
|
|
3
|
-
import android.annotation.SuppressLint;
|
|
4
|
-
import android.graphics.Bitmap;
|
|
5
|
-
import android.media.MediaMetadataRetriever;
|
|
6
|
-
import android.util.Log;
|
|
7
|
-
|
|
8
|
-
import com.arthenica.ffmpegkit.FFmpegKit;
|
|
9
|
-
import com.arthenica.ffmpegkit.FFmpegSession;
|
|
10
|
-
import com.arthenica.ffmpegkit.ReturnCode;
|
|
11
|
-
import com.arthenica.ffmpegkit.SessionState;
|
|
12
|
-
import com.facebook.react.bridge.Arguments;
|
|
13
|
-
import com.facebook.react.bridge.WritableMap;
|
|
14
|
-
import com.videotrim.enums.ErrorCode;
|
|
15
|
-
import com.videotrim.interfaces.VideoTrimListener;
|
|
16
|
-
|
|
17
|
-
import java.text.SimpleDateFormat;
|
|
18
|
-
import java.util.ArrayList;
|
|
19
|
-
import java.util.Date;
|
|
20
|
-
import java.util.List;
|
|
21
|
-
import java.util.TimeZone;
|
|
22
|
-
|
|
23
|
-
import iknow.android.utils.DeviceUtil;
|
|
24
|
-
import iknow.android.utils.UnitConverter;
|
|
25
|
-
import iknow.android.utils.callback.SingleCallback;
|
|
26
|
-
import iknow.android.utils.thread.BackgroundExecutor;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
public class VideoTrimmerUtil {
|
|
30
|
-
|
|
31
|
-
private static final String TAG = VideoTrimmerUtil.class.getSimpleName();
|
|
32
|
-
public static final String FILE_PREFIX = "trimmedVideo";
|
|
33
|
-
public static final long MIN_SHOOT_DURATION = 1000L;// min 3 seconds for trimming
|
|
34
|
-
public static final int VIDEO_MAX_TIME = 10;// max 10 seconds for trimming
|
|
35
|
-
public static final long MAX_SHOOT_DURATION = VIDEO_MAX_TIME * 1000L;
|
|
36
|
-
// public static long maxShootDuration = 10 * 1000L;
|
|
37
|
-
public static int MAX_COUNT_RANGE = 10; // how many images in the highlight range of seek bar
|
|
38
|
-
public static int SCREEN_WIDTH_FULL = DeviceUtil.getDeviceWidth();
|
|
39
|
-
public static final int RECYCLER_VIEW_PADDING = UnitConverter.dpToPx(35);
|
|
40
|
-
public static String DEFAULT_AUDIO_EXTENSION = ".wav";
|
|
41
|
-
public static int VIDEO_FRAMES_WIDTH = SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
|
|
42
|
-
// public static final int THUMB_WIDTH = (SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2) / VIDEO_MAX_TIME;
|
|
43
|
-
public static int mThumbWidth = 0; // make it automatic
|
|
44
|
-
public static final int THUMB_HEIGHT = UnitConverter.dpToPx(50); // x2 for better resolution
|
|
45
|
-
public static final int THUMB_WIDTH = UnitConverter.dpToPx(25); // x2 for better resolution
|
|
46
|
-
private static final int THUMB_RESOLUTION_RES = 2; // double thumb resolution for better quality
|
|
47
|
-
|
|
48
|
-
public static FFmpegSession trim(String inputFile, String outputFile, int videoDuration, long startMs, long endMs, boolean enableRotation, double rotationAngle, final VideoTrimListener callback) {
|
|
49
|
-
// Get the current date and time
|
|
50
|
-
Date currentDate = new Date();
|
|
51
|
-
|
|
52
|
-
// Create a SimpleDateFormat object with the desired format
|
|
53
|
-
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
|
54
|
-
|
|
55
|
-
// Set the timezone to UTC
|
|
56
|
-
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
57
|
-
// Format the current date and time
|
|
58
|
-
String formattedDateTime = dateFormat.format(currentDate);
|
|
59
|
-
|
|
60
|
-
// create list to store commands
|
|
61
|
-
List<String> cmds = new ArrayList<>();
|
|
62
|
-
cmds.add("-ss");
|
|
63
|
-
cmds.add(startMs + "ms");
|
|
64
|
-
cmds.add("-to");
|
|
65
|
-
cmds.add(endMs + "ms");
|
|
66
|
-
|
|
67
|
-
if (enableRotation) {
|
|
68
|
-
// add "-display_rotation" and rotation angle to the command, contact, not creating new
|
|
69
|
-
cmds.add("-display_rotation");
|
|
70
|
-
cmds.add(String.valueOf(rotationAngle));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
cmds.add("-i");
|
|
74
|
-
cmds.add(inputFile);
|
|
75
|
-
cmds.add("-c");
|
|
76
|
-
cmds.add("copy");
|
|
77
|
-
cmds.add("-metadata");
|
|
78
|
-
cmds.add("creation_time=" + formattedDateTime);
|
|
79
|
-
cmds.add(outputFile);
|
|
80
|
-
|
|
81
|
-
String[] command = cmds.toArray(new String[0]);
|
|
82
|
-
String cmdStr = "Command: " + String.join(" ", command);
|
|
83
|
-
|
|
84
|
-
Log.d(TAG, cmdStr);
|
|
85
|
-
|
|
86
|
-
WritableMap m = Arguments.createMap();
|
|
87
|
-
m.putString("message", cmdStr);
|
|
88
|
-
|
|
89
|
-
callback.onLog(m);
|
|
90
|
-
|
|
91
|
-
return FFmpegKit.executeWithArgumentsAsync(command, session -> {
|
|
92
|
-
SessionState state = session.getState();
|
|
93
|
-
ReturnCode returnCode = session.getReturnCode();
|
|
94
|
-
if (ReturnCode.isSuccess(session.getReturnCode())) {
|
|
95
|
-
// SUCCESS
|
|
96
|
-
callback.onFinishTrim(outputFile, startMs, endMs, videoDuration);
|
|
97
|
-
} else if (ReturnCode.isCancel(session.getReturnCode())) {
|
|
98
|
-
// CANCEL
|
|
99
|
-
callback.onCancelTrim();
|
|
100
|
-
} else {
|
|
101
|
-
// FAILURE
|
|
102
|
-
String errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
|
|
103
|
-
callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED);
|
|
104
|
-
}
|
|
105
|
-
}, log -> {
|
|
106
|
-
Log.d(TAG, "FFmpeg process started with log " + log.getMessage());
|
|
107
|
-
|
|
108
|
-
WritableMap map = Arguments.createMap();
|
|
109
|
-
map.putInt("level", log.getLevel().getValue());
|
|
110
|
-
map.putString("message", log.getMessage());
|
|
111
|
-
map.putDouble("sessionId", log.getSessionId());
|
|
112
|
-
map.putString("logStr", log.toString());
|
|
113
|
-
|
|
114
|
-
callback.onLog(map);
|
|
115
|
-
}, statistics -> {
|
|
116
|
-
int timeInMilliseconds = (int) statistics.getTime();
|
|
117
|
-
if (timeInMilliseconds > 0) {
|
|
118
|
-
int completePercentage =
|
|
119
|
-
(timeInMilliseconds * 100) / videoDuration;
|
|
120
|
-
callback.onTrimmingProgress(Math.min(Math.max(completePercentage, 0), 100));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
WritableMap map = Arguments.createMap();
|
|
124
|
-
map.putDouble("sessionId", statistics.getSessionId());
|
|
125
|
-
map.putInt("videoFrameNumber", statistics.getVideoFrameNumber());
|
|
126
|
-
map.putDouble("videoFps", statistics.getVideoFps());
|
|
127
|
-
map.putDouble("videoQuality", statistics.getVideoQuality());
|
|
128
|
-
map.putDouble("size", statistics.getSize());
|
|
129
|
-
map.putDouble("time", statistics.getTime());
|
|
130
|
-
map.putDouble("bitrate", statistics.getBitrate());
|
|
131
|
-
map.putDouble("speed", statistics.getSpeed());
|
|
132
|
-
map.putString("statisticsStr", statistics.toString());
|
|
133
|
-
callback.onStatistics(map);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
public static void shootVideoThumbInBackground(final MediaMetadataRetriever mediaMetadataRetriever, final int totalThumbsCount, final long startPosition,
|
|
138
|
-
final long endPosition, final SingleCallback<Bitmap, Integer> callback) {
|
|
139
|
-
BackgroundExecutor.execute(new BackgroundExecutor.Task("", 0L, "") {
|
|
140
|
-
@Override public void execute() {
|
|
141
|
-
try {
|
|
142
|
-
// Retrieve media data use microsecond
|
|
143
|
-
long interval = (endPosition - startPosition) / (totalThumbsCount - 1);
|
|
144
|
-
for (long i = 0; i < totalThumbsCount; ++i) {
|
|
145
|
-
long frameTime = startPosition + interval * i;
|
|
146
|
-
|
|
147
|
-
Bitmap bitmap;
|
|
148
|
-
try {
|
|
149
|
-
bitmap = mediaMetadataRetriever.getFrameAtTime(frameTime * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
|
150
|
-
} catch (final Throwable t) {
|
|
151
|
-
// this can happen while thumbnails are being generated in background and we press Cancel
|
|
152
|
-
t.printStackTrace();
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if(bitmap == null) continue;
|
|
157
|
-
try {
|
|
158
|
-
bitmap = Bitmap.createScaledBitmap(bitmap, mThumbWidth * THUMB_RESOLUTION_RES, THUMB_HEIGHT * THUMB_RESOLUTION_RES, false);
|
|
159
|
-
} catch (final Throwable t) {
|
|
160
|
-
t.printStackTrace();
|
|
161
|
-
}
|
|
162
|
-
callback.onSingleCallback(bitmap, (int) interval);
|
|
163
|
-
}
|
|
164
|
-
} catch (final Throwable e) {
|
|
165
|
-
Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|