react-native-video-trim 0.0.1 → 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/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 +315 -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 +9 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +277 -0
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +109 -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 +444 -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
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Mai Trung Duc
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# react-native-video-trim
|
|
2
|
+
<div align="center">
|
|
3
|
+
<h2>Video trimmer for your React Native app</h2>
|
|
4
|
+
|
|
5
|
+
<img src="images/android.gif" width="300" />
|
|
6
|
+
<img src="images/ios.gif" width="300" />
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install react-native-video-trim
|
|
13
|
+
|
|
14
|
+
# or with yarn
|
|
15
|
+
|
|
16
|
+
yarn add react-native-video-trim
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Next install CocoaPods deps:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
cd ios & pod install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { showEditor } from 'react-native-video-trim';
|
|
29
|
+
|
|
30
|
+
// ...
|
|
31
|
+
|
|
32
|
+
showEditor(videoUrl);
|
|
33
|
+
|
|
34
|
+
// or with output length limit
|
|
35
|
+
|
|
36
|
+
showEditor(videoUrl, {
|
|
37
|
+
maxDuration: 20,
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
Usually this library will be used along with other library to select video file, Eg. [react-native-image-picker](https://github.com/react-native-image-picker/react-native-image-picker). Below is real world example:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import * as React from 'react';
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
StyleSheet,
|
|
47
|
+
View,
|
|
48
|
+
Text,
|
|
49
|
+
TouchableOpacity,
|
|
50
|
+
NativeEventEmitter,
|
|
51
|
+
NativeModules,
|
|
52
|
+
} from 'react-native';
|
|
53
|
+
import { isValidVideo, showEditor } from 'react-native-video-trim';
|
|
54
|
+
import { launchImageLibrary } from 'react-native-image-picker';
|
|
55
|
+
import { useEffect } from 'react';
|
|
56
|
+
|
|
57
|
+
export default function App() {
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
|
|
60
|
+
const subscription = eventEmitter.addListener('VideoTrim', (event) => {
|
|
61
|
+
switch (event.name) {
|
|
62
|
+
case 'onShow': {
|
|
63
|
+
console.log('onShowListener', event);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'onHide': {
|
|
67
|
+
console.log('onHide', event);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case 'onStartTrimming': {
|
|
71
|
+
console.log('onStartTrimming', event);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 'onFinishTrimming': {
|
|
75
|
+
console.log('onFinishTrimming', event);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'onCancelTrimming': {
|
|
79
|
+
console.log('onCancelTrimming', event);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'onError': {
|
|
83
|
+
console.log('onError', event);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
subscription.remove();
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View style={styles.container}>
|
|
96
|
+
<TouchableOpacity
|
|
97
|
+
onPress={async () => {
|
|
98
|
+
const result = await launchImageLibrary({
|
|
99
|
+
mediaType: 'video',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
isValidVideo(result.assets![0]?.uri || '').then((res) =>
|
|
103
|
+
console.log(res)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
showEditor(result.assets![0]?.uri || '', {
|
|
107
|
+
maxDuration: 20,
|
|
108
|
+
});
|
|
109
|
+
}}
|
|
110
|
+
style={{ padding: 10, backgroundColor: 'red' }}
|
|
111
|
+
>
|
|
112
|
+
<Text>Launch Library</Text>
|
|
113
|
+
</TouchableOpacity>
|
|
114
|
+
<TouchableOpacity
|
|
115
|
+
onPress={() => {
|
|
116
|
+
isValidVideo('invalid file path').then((res) => console.log(res));
|
|
117
|
+
}}
|
|
118
|
+
style={{
|
|
119
|
+
padding: 10,
|
|
120
|
+
backgroundColor: 'blue',
|
|
121
|
+
marginTop: 20,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<Text>Check Video Valid</Text>
|
|
125
|
+
</TouchableOpacity>
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const styles = StyleSheet.create({
|
|
131
|
+
container: {
|
|
132
|
+
flex: 1,
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
justifyContent: 'center',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
# Methods
|
|
140
|
+
|
|
141
|
+
## showEditor(videoPath: string, config?: EditorConfig)
|
|
142
|
+
Main method to show Video Editor UI.
|
|
143
|
+
|
|
144
|
+
*Params*:
|
|
145
|
+
- `videoPath`: Path to video file, if this is an invalid path, `onError` event will be fired
|
|
146
|
+
- `config` (optional):
|
|
147
|
+
|
|
148
|
+
- `saveToPhoto` (optional, `default = true`): whether to save video to photo/gallery after editing
|
|
149
|
+
- `maxDuration` (optional): maximum duration for the trimmed video
|
|
150
|
+
|
|
151
|
+
If `saveToPhoto = true`, you must ensure that you have request permission to write to photo/gallery
|
|
152
|
+
- For Android: you need to have `<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />` in AndroidManifest.xml
|
|
153
|
+
- For iOS: you need `NSPhotoLibraryUsageDescription` in Info.plist
|
|
154
|
+
|
|
155
|
+
## isValidVideo (videoPath: string)
|
|
156
|
+
|
|
157
|
+
This method is to check if a path is a actual video. It returns `Promise<boolean>`
|
|
158
|
+
|
|
159
|
+
# Events
|
|
160
|
+
To listen for events you interest, do the following:
|
|
161
|
+
```js
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const eventEmitter = new NativeEventEmitter(NativeModules.VideoTrim);
|
|
164
|
+
const subscription = eventEmitter.addListener('VideoTrim', (event) => {
|
|
165
|
+
switch (event.name) {
|
|
166
|
+
case 'onShow': {
|
|
167
|
+
// on Dialog show
|
|
168
|
+
console.log('onShowListener', event);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'onHide': {
|
|
172
|
+
// on Dialog hide
|
|
173
|
+
console.log('onHide', event);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'onStartTrimming': {
|
|
177
|
+
// Android only: on start trimming
|
|
178
|
+
console.log('onStartTrimming', event);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'onFinishTrimming': {
|
|
182
|
+
// on trimming is done
|
|
183
|
+
console.log('onFinishTrimming', event);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'onCancelTrimming': {
|
|
187
|
+
// when user clicks Cancel button
|
|
188
|
+
console.log('onCancelTrimming', event);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'onError': {
|
|
192
|
+
// any error occured: invalid file, lack of permissions to write to photo/gallery, unexpected error...
|
|
193
|
+
console.log('onError', event);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
subscription.remove();
|
|
201
|
+
};
|
|
202
|
+
}, []);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
# Thanks
|
|
206
|
+
Android part is created by modified + fix bugs from [original project](Android-Video-Trimmer)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
dependencies {
|
|
8
|
+
classpath "com.android.tools.build:gradle:7.2.1"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def isNewArchitectureEnabled() {
|
|
13
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
apply plugin: "com.android.library"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
|
|
20
|
+
|
|
21
|
+
if (isNewArchitectureEnabled()) {
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def getExtOrDefault(name) {
|
|
26
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["VideoTrim_" + name]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def getExtOrIntegerDefault(name) {
|
|
30
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["VideoTrim_" + name]).toInteger()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def supportsNamespace() {
|
|
34
|
+
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
|
|
35
|
+
def major = parsed[0].toInteger()
|
|
36
|
+
def minor = parsed[1].toInteger()
|
|
37
|
+
|
|
38
|
+
// Namespace support was added in 7.3.0
|
|
39
|
+
if (major == 7 && minor >= 3) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return major >= 8
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
android {
|
|
47
|
+
if (supportsNamespace()) {
|
|
48
|
+
namespace "com.videotrim"
|
|
49
|
+
} else {
|
|
50
|
+
sourceSets {
|
|
51
|
+
main {
|
|
52
|
+
manifest.srcFile "src/main/AndroidManifestDeprecated.xml"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
58
|
+
|
|
59
|
+
defaultConfig {
|
|
60
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
61
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
62
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
63
|
+
}
|
|
64
|
+
buildTypes {
|
|
65
|
+
release {
|
|
66
|
+
minifyEnabled false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lintOptions {
|
|
71
|
+
disable "GradleCompatible"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
compileOptions {
|
|
75
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
76
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
repositories {
|
|
82
|
+
mavenCentral()
|
|
83
|
+
google()
|
|
84
|
+
jcenter()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
dependencies {
|
|
89
|
+
// For < 0.71, this will be from the local maven repo
|
|
90
|
+
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
91
|
+
//noinspection GradleDynamicVersion
|
|
92
|
+
implementation "com.facebook.react:react-native:+"
|
|
93
|
+
implementation 'com.github.iknow4:android-utils-sdk:1.1.2'
|
|
94
|
+
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
|
95
|
+
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
|
|
96
|
+
implementation 'com.arthenica:ffmpeg-kit-full:5.1.LTS'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isNewArchitectureEnabled()) {
|
|
100
|
+
react {
|
|
101
|
+
jsRootDir = file("../src/")
|
|
102
|
+
libraryName = "VideoTrim"
|
|
103
|
+
codegenJavaPackageName = "com.videotrim"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
package com.videotrim;
|
|
2
|
+
|
|
3
|
+
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
|
4
|
+
|
|
5
|
+
import android.Manifest;
|
|
6
|
+
import android.app.Activity;
|
|
7
|
+
import android.content.res.ColorStateList;
|
|
8
|
+
import android.graphics.Color;
|
|
9
|
+
import android.media.MediaMetadataRetriever;
|
|
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;
|
|
17
|
+
|
|
18
|
+
import androidx.annotation.NonNull;
|
|
19
|
+
import androidx.annotation.Nullable;
|
|
20
|
+
import androidx.appcompat.app.AlertDialog;
|
|
21
|
+
import androidx.fragment.app.FragmentActivity;
|
|
22
|
+
|
|
23
|
+
import com.facebook.react.bridge.Arguments;
|
|
24
|
+
import com.facebook.react.bridge.LifecycleEventListener;
|
|
25
|
+
import com.facebook.react.bridge.Promise;
|
|
26
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
27
|
+
import com.facebook.react.bridge.ReactContext;
|
|
28
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
29
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
30
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
31
|
+
import com.facebook.react.bridge.WritableMap;
|
|
32
|
+
import com.facebook.react.module.annotations.ReactModule;
|
|
33
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
34
|
+
import com.permissionx.guolindev.PermissionX;
|
|
35
|
+
import com.videotrim.interfaces.VideoTrimListener;
|
|
36
|
+
import com.videotrim.utils.StorageUtil;
|
|
37
|
+
import com.videotrim.widgets.VideoTrimmerView;
|
|
38
|
+
|
|
39
|
+
import java.io.File;
|
|
40
|
+
import java.io.IOException;
|
|
41
|
+
import iknow.android.utils.BaseUtils;
|
|
42
|
+
|
|
43
|
+
@ReactModule(name = VideoTrimModule.NAME)
|
|
44
|
+
public class VideoTrimModule extends ReactContextBaseJavaModule implements VideoTrimListener, LifecycleEventListener {
|
|
45
|
+
public static final String NAME = "VideoTrim";
|
|
46
|
+
private static Boolean isInit = false;
|
|
47
|
+
private VideoTrimmerView trimmerView;
|
|
48
|
+
private AlertDialog alertDialog;
|
|
49
|
+
private AlertDialog mProgressDialog;
|
|
50
|
+
private ProgressBar mProgressBar;
|
|
51
|
+
private Boolean mSaveToPhoto = true;
|
|
52
|
+
private int mMaxDuration = 0;
|
|
53
|
+
private int listenerCount = 0;
|
|
54
|
+
|
|
55
|
+
public VideoTrimModule(ReactApplicationContext reactContext) {
|
|
56
|
+
super(reactContext);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Override
|
|
60
|
+
@NonNull
|
|
61
|
+
public String getName() {
|
|
62
|
+
return NAME;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@ReactMethod
|
|
67
|
+
public void showEditor(String videoPath, ReadableMap config) {
|
|
68
|
+
if (trimmerView != null || alertDialog != null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (config.hasKey("saveToPhoto")) {
|
|
73
|
+
this.mSaveToPhoto = config.getBoolean("saveToPhoto");
|
|
74
|
+
}
|
|
75
|
+
if (config.hasKey("maxDuration")) {
|
|
76
|
+
this.mMaxDuration = config.getInt("maxDuration");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!_isValidVideo(videoPath)) {
|
|
80
|
+
WritableMap map = Arguments.createMap();
|
|
81
|
+
map.putString("message", "File is not a valid video");
|
|
82
|
+
sendEvent(getReactApplicationContext(), "onError", map);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Activity activity = getReactApplicationContext().getCurrentActivity();
|
|
87
|
+
|
|
88
|
+
if (!isInit) {
|
|
89
|
+
init(activity);
|
|
90
|
+
isInit = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// here is NOT main thread, we need to create VideoTrimmerView on UI thread, so that later we can update it using same thread
|
|
94
|
+
|
|
95
|
+
runOnUiThread(() -> {
|
|
96
|
+
trimmerView = new VideoTrimmerView(getReactApplicationContext(), mMaxDuration, null);
|
|
97
|
+
trimmerView.setOnTrimVideoListener(this);
|
|
98
|
+
trimmerView.initVideoByURI(Uri.parse(videoPath));
|
|
99
|
+
|
|
100
|
+
AlertDialog.Builder builder = new AlertDialog.Builder(activity, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
|
|
101
|
+
builder.setCancelable(false);
|
|
102
|
+
alertDialog = builder.create();
|
|
103
|
+
alertDialog.setView(trimmerView);
|
|
104
|
+
alertDialog.show();
|
|
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
|
+
|
|
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)
|
|
118
|
+
alertDialog.setOnDismissListener(dialog -> {
|
|
119
|
+
// This is called in same thread as the trimmer view -> UI thread
|
|
120
|
+
if (trimmerView != null) {
|
|
121
|
+
trimmerView.onDestroy();
|
|
122
|
+
trimmerView = null;
|
|
123
|
+
}
|
|
124
|
+
hideDialog();
|
|
125
|
+
sendEvent(getReactApplicationContext(), "onHide", null);
|
|
126
|
+
});
|
|
127
|
+
sendEvent(getReactApplicationContext(), "onShow", null);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private void init(Activity activity) {
|
|
132
|
+
isInit = true;
|
|
133
|
+
// we have to init this before create videoTrimmerView
|
|
134
|
+
BaseUtils.init(getReactApplicationContext());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Override
|
|
138
|
+
public void onHostResume() {
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Override
|
|
143
|
+
public void onHostPause() {
|
|
144
|
+
if (trimmerView != null) {
|
|
145
|
+
trimmerView.onVideoPause();
|
|
146
|
+
trimmerView.setRestoreState(true);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Override
|
|
151
|
+
public void onHostDestroy() {
|
|
152
|
+
hideDialog();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Override public void onStartTrim() {
|
|
156
|
+
sendEvent(getReactApplicationContext(), "onStartTrimming", null);
|
|
157
|
+
runOnUiThread(() -> {
|
|
158
|
+
buildDialog();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
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
|
+
|
|
171
|
+
@Override public void onFinishTrim(String in) {
|
|
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);
|
|
202
|
+
}
|
|
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);
|
|
210
|
+
this.hideDialog();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Override public void onCancel() {
|
|
214
|
+
sendEvent(getReactApplicationContext(), "onCancelTrimming", null);
|
|
215
|
+
this.hideDialog();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private void hideDialog() {
|
|
219
|
+
if (mProgressDialog != null) {
|
|
220
|
+
if (mProgressDialog.isShowing()) mProgressDialog.dismiss();
|
|
221
|
+
mProgressBar = null;
|
|
222
|
+
mProgressDialog = null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (alertDialog != null) {
|
|
226
|
+
if(alertDialog.isShowing()) {
|
|
227
|
+
alertDialog.dismiss();
|
|
228
|
+
}
|
|
229
|
+
alertDialog = null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
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();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@ReactMethod
|
|
275
|
+
public void addListener(String eventName) {
|
|
276
|
+
// This method is required by React
|
|
277
|
+
listenerCount += 1;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@ReactMethod
|
|
281
|
+
public void removeListeners(Integer count) {
|
|
282
|
+
// This method is required by React
|
|
283
|
+
listenerCount -= count;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
|
|
287
|
+
if (listenerCount > 0) {
|
|
288
|
+
WritableMap map = params != null ? params : Arguments.createMap();
|
|
289
|
+
map.putString("name", eventName);
|
|
290
|
+
reactContext
|
|
291
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
292
|
+
.emit("VideoTrim", map);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
public boolean _isValidVideo(String filePath) {
|
|
298
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
retriever.setDataSource(getReactApplicationContext(), Uri.parse(filePath));
|
|
302
|
+
} catch (Exception e){
|
|
303
|
+
e.printStackTrace();
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
String hasVideo = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
|
|
308
|
+
return "yes".equals(hasVideo);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@ReactMethod
|
|
312
|
+
private void isValidVideo(String filePath, Promise promise) {
|
|
313
|
+
promise.resolve(_isValidVideo(filePath));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.videotrim;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
public class VideoTrimPackage implements ReactPackage {
|
|
15
|
+
@NonNull
|
|
16
|
+
@Override
|
|
17
|
+
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
|
18
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
19
|
+
modules.add(new VideoTrimModule(reactContext));
|
|
20
|
+
return modules;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@NonNull
|
|
24
|
+
@Override
|
|
25
|
+
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
|
26
|
+
return Collections.emptyList();
|
|
27
|
+
}
|
|
28
|
+
}
|