Skip to content

Commit 77d5040

Browse files
kesha-antonovShahen Hovhannisyan
authored andcommitted
feat(android): Use FFPEG instead of Mp4parser, added new Crop method. added format to "getPreviewImage": "JPEG"/"base64" (#63)
* image instead of base64 * crop * merge. update gitignore * fix crop * handle invalid crop * fix identation * ios + android: get base64 or JPEG (added). unify method names + some refactoring * android: crop with ffmpeg * add ffmpeg v3.3 * android: fix crop + trim. ios: add todos * android: added note to readme * fix undefined var * ios: crop fix
1 parent 451127d commit 77d5040

12 files changed

Lines changed: 788 additions & 293 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ jspm_packages
5858

5959
# Optional REPL history
6060
.node_repl_history
61+
62+
.DS_Store

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ project(':react-native-video-processing').projectDir = new File(rootProject.proj
3030
compile project(':react-native-video-processing')
3131
```
3232

33+
6. Add this add the end of the file `android/app/build.gradle` (need it to download and compile ffmpeg lib):
34+
```
35+
allprojects {
36+
repositories {
37+
maven { url "https://jitpack.io" }
38+
}
39+
}
40+
```
41+
3342
#### [iOS]
3443

3544
1. In Xcode, click the "Add Files to <your-project-name>".
@@ -178,3 +187,4 @@ export class App extends Component {
178187
4. [x] More processing options
179188
5. [ ] Create native trimmer component for Android
180189
6. [x] Provide Standalone API
190+
7. [ ] Describe API methods with parameters in README

android/build.gradle

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
apply plugin: 'com.android.library'
32

43
android {
@@ -19,9 +18,23 @@ android {
1918
}
2019
}
2120

21+
configurations.all {
22+
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
23+
}
24+
25+
2226
dependencies {
2327
compile 'com.facebook.react:react-native:0.20.+'
2428
compile 'com.yqritc:android-scalablevideoview:1.0.4'
2529
compile 'com.googlecode.mp4parser:isoparser:1.1.20'
2630
compile 'com.github.wseemann:FFmpegMediaMetadataRetriever:1.0.14'
31+
compile 'com.github.kesha-antonov:ffmpeg-android-java:e69ea470d69d271fafbd03d0a4b0ea67d6731ccc'
32+
}
33+
34+
allprojects {
35+
repositories {
36+
mavenLocal()
37+
jcenter()
38+
maven { url "https://jitpack.io" }
39+
}
2740
}

android/src/main/java/com/shahenlibrary/RNVideoProcessingPackage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,4 @@ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext
5454
new VideoPlayerViewManager()
5555
);
5656
}
57-
}
57+
}

android/src/main/java/com/shahenlibrary/Trimmer/Trimmer.java

Lines changed: 220 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,22 @@
5151

5252
import wseemann.media.FFmpegMediaMetadataRetriever;
5353

54+
import java.util.UUID;
55+
import java.io.FileOutputStream;
56+
import java.util.Arrays;
57+
import java.util.ArrayList;
58+
59+
import com.github.hiteshsondhi88.libffmpeg.FFmpeg;
60+
import com.github.hiteshsondhi88.libffmpeg.FFmpegExecuteResponseHandler;
61+
import com.github.hiteshsondhi88.libffmpeg.FFmpegLoadBinaryResponseHandler;
62+
63+
5464
public class Trimmer {
5565

5666
private static final String LOG_TAG = "RNTrimmerManager";
5767

68+
private static boolean ffmpegLoaded;
69+
5870
public static void getPreviewImages(String path, Promise promise, ReactApplicationContext ctx) {
5971
FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
6072
if (VideoEdit.shouldUseURI(path)) {
@@ -202,19 +214,222 @@ public void cancelAction() {
202214
}
203215
}
204216

205-
static void getPreviewAtPosition(String source, double sec, final Promise promise) {
217+
static File createTempFile(String extension, final Promise promise, ReactApplicationContext ctx) {
218+
UUID uuid = UUID.randomUUID();
219+
String imageName = uuid.toString() + "-screenshot";
220+
221+
File cacheDir = ctx.getCacheDir();
222+
File tempFile = null;
223+
try {
224+
tempFile = File.createTempFile(imageName, "." + extension, cacheDir);
225+
} catch( IOException e ) {
226+
promise.reject("Failed to create temp file", e.toString());
227+
return null;
228+
}
229+
230+
if (tempFile.exists()) {
231+
tempFile.delete();
232+
}
233+
234+
return tempFile;
235+
}
236+
237+
static void getPreviewImageAtPosition(String source, double sec, String format, final Promise promise, ReactApplicationContext ctx) {
206238
FFmpegMediaMetadataRetriever metadataRetriever = new FFmpegMediaMetadataRetriever();
239+
FFmpegMediaMetadataRetriever.IN_PREFERRED_CONFIG = Bitmap.Config.ARGB_8888;
207240
metadataRetriever.setDataSource(source);
208241

209242
Bitmap bmp = metadataRetriever.getFrameAtTime((long) (sec * 1000000));
243+
244+
// NOTE: FIX ROTATED BITMAP
245+
int orientation = Integer.parseInt( metadataRetriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) );
246+
metadataRetriever.release();
247+
248+
if ( orientation != 0 ) {
249+
Matrix matrix = new Matrix();
250+
matrix.postRotate(orientation);
251+
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
252+
}
253+
210254
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
211-
bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
212-
byte[] byteArray = byteArrayOutputStream .toByteArray();
213-
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);
214255

215256
WritableMap event = Arguments.createMap();
216-
event.putString("image", encoded);
257+
258+
if ( format.equals(null) || format.equals("base64") ) {
259+
bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
260+
byte[] byteArray = byteArrayOutputStream .toByteArray();
261+
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);
262+
263+
event.putString("image", encoded);
264+
} else if ( format.equals("JPEG") ) {
265+
bmp.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
266+
byte[] byteArray = byteArrayOutputStream.toByteArray();
267+
268+
File tempFile = createTempFile("jpeg", promise, ctx);
269+
270+
try {
271+
FileOutputStream fos = new FileOutputStream( tempFile.getPath() );
272+
273+
fos.write( byteArray );
274+
fos.close();
275+
} catch (java.io.IOException e) {
276+
promise.reject("Failed to save image", e.toString());
277+
return;
278+
}
279+
280+
WritableMap imageMap = Arguments.createMap();
281+
imageMap.putString("uri", "file://" + tempFile.getPath());
282+
283+
event.putMap("image", imageMap);
284+
} else {
285+
promise.reject("Wrong format error", "Wrong 'format'. Expected one of 'base64' or 'JPEG'.");
286+
return;
287+
}
217288

218289
promise.resolve(event);
219290
}
291+
292+
static void crop(String source, ReadableMap options, final Promise promise, ReactApplicationContext ctx) {
293+
int cropWidth = (int)( options.getDouble("cropWidth") );
294+
int cropHeight = (int)( options.getDouble("cropHeight") );
295+
int cropOffsetX = (int)( options.getDouble("cropOffsetX") );
296+
int cropOffsetY = (int)( options.getDouble("cropOffsetY") );
297+
298+
FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
299+
if (VideoEdit.shouldUseURI(source)) {
300+
retriever.setDataSource(ctx, Uri.parse(source));
301+
} else {
302+
retriever.setDataSource(source);
303+
}
304+
305+
int videoWidth = Integer.parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
306+
int videoHeight = Integer.parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
307+
retriever.release();
308+
309+
// NOTE: FFMpeg CROP NEED TO BE DEVIDED BY 2. OR YOU WILL SEE BLANK WHITE LINES FROM LEFT/RIGHT
310+
while( cropWidth % 2 > 0 && cropWidth < videoWidth ) {
311+
cropWidth += 1;
312+
}
313+
while( cropWidth % 2 > 0 && cropWidth > 0 ) {
314+
cropWidth -= 1;
315+
}
316+
while( cropHeight % 2 > 0 && cropHeight < videoHeight ) {
317+
cropHeight += 1;
318+
}
319+
while( cropHeight % 2 > 0 && cropHeight > 0 ) {
320+
cropHeight -= 1;
321+
}
322+
323+
// TODO: 1) ADD METHOD TO CHECK "IS FFMPEG LOADED".
324+
// 2) CHECK IT HERE
325+
// 3) EXPORT THAT METHOD TO "JS"
326+
327+
final File tempFile = createTempFile("mp4", promise, ctx);
328+
329+
ArrayList<String> cmd = new ArrayList<String>();
330+
cmd.add("-y"); // NOTE: OVERWRITE OUTPUT FILE
331+
332+
String startTime = options.getString("startTime");
333+
if ( !startTime.equals(null) && !startTime.equals("") ) {
334+
cmd.add("-ss");
335+
cmd.add(startTime);
336+
}
337+
338+
// NOTE: INPUT FILE
339+
cmd.add("-i");
340+
cmd.add(source);
341+
342+
String endTime = options.getString("endTime");
343+
if ( !endTime.equals(null) && !endTime.equals("") ) {
344+
cmd.add("-to");
345+
cmd.add(endTime);
346+
}
347+
348+
cmd.add("-vf");
349+
cmd.add("crop=" + Integer.toString(cropWidth) + ":" + Integer.toString(cropHeight) + ":" + Integer.toString(cropOffsetX) + ":" + Integer.toString(cropOffsetY));
350+
351+
cmd.add("-preset");
352+
cmd.add("ultrafast");
353+
// NOTE: DO NOT CONVERT AUDIO TO SAVE TIME
354+
cmd.add("-c:a");
355+
cmd.add("copy");
356+
// NOTE: FLAG TO CONVER "AAC" AUDIO CODEC
357+
cmd.add("-strict");
358+
cmd.add("-2");
359+
// NOTE: OUTPUT FILE
360+
cmd.add(tempFile.getPath());
361+
362+
final String[] cmdToExec = cmd.toArray( new String[0] );
363+
364+
Log.d(LOG_TAG, Arrays.toString(cmdToExec));
365+
366+
try {
367+
FFmpeg.getInstance(ctx).execute(cmdToExec, new FFmpegExecuteResponseHandler() {
368+
369+
@Override
370+
public void onStart() {
371+
Log.d(LOG_TAG, "crop: onStart");
372+
}
373+
374+
@Override
375+
public void onProgress(String message) {
376+
Log.d(LOG_TAG, "crop: onProgress");
377+
}
378+
379+
@Override
380+
public void onFailure(String message) {
381+
Log.d(LOG_TAG, "crop: onFailure");
382+
promise.reject("Crop error: failed.", message);
383+
}
384+
385+
@Override
386+
public void onSuccess(String message) {
387+
Log.d(LOG_TAG, "crop: onSuccess");
388+
Log.d(LOG_TAG, message);
389+
390+
WritableMap event = Arguments.createMap();
391+
event.putString("source", "file://" + tempFile.getPath());
392+
promise.resolve(event);
393+
}
394+
395+
@Override
396+
public void onFinish() {
397+
Log.d(LOG_TAG, "crop: onFinish");
398+
}
399+
});
400+
} catch (Exception e) {
401+
promise.reject("Crop error. Command already running", e.toString());
402+
}
403+
}
404+
405+
public static void loadFfmpeg(ReactApplicationContext ctx){
406+
try {
407+
FFmpeg.getInstance(ctx).loadBinary(new FFmpegLoadBinaryResponseHandler() {
408+
@Override
409+
public void onStart() {
410+
Log.d(LOG_TAG, "load FFMPEG: onStart");
411+
}
412+
413+
@Override
414+
public void onSuccess() {
415+
Log.d(LOG_TAG, "load FFMPEG: onSuccess");
416+
ffmpegLoaded = true;
417+
}
418+
419+
@Override
420+
public void onFailure() {
421+
ffmpegLoaded = false;
422+
Log.d(LOG_TAG, "load FFMPEG: Failed to load ffmpeg");
423+
}
424+
425+
@Override
426+
public void onFinish() {
427+
Log.d(LOG_TAG, "load FFMPEG: onFinish");
428+
}
429+
});
430+
} catch (Exception e){
431+
ffmpegLoaded = false;
432+
Log.d("Failed to load ffmpeg", e.toString());
433+
}
434+
}
220435
}

android/src/main/java/com/shahenlibrary/Trimmer/TrimmerManager.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class TrimmerManager extends ReactContextBaseJavaModule {
4040
public TrimmerManager(ReactApplicationContext reactContext) {
4141
super(reactContext);
4242
this.reactContext = reactContext;
43+
loadFfmpeg();
4344
}
4445

4546
@Override
@@ -74,6 +75,17 @@ public void compress(ReadableMap options, Promise promise) {
7475
public void getPreviewImageAtPosition(ReadableMap options, Promise promise) {
7576
String source = options.getString("source");
7677
double sec = options.getDouble("second");
77-
Trimmer.getPreviewAtPosition(source, sec, promise);
78+
String format = options.getString("format");
79+
Trimmer.getPreviewImageAtPosition(source, sec, format, promise, reactContext);
80+
}
81+
82+
@ReactMethod
83+
public void crop(String path, ReadableMap options, Promise promise) {
84+
Trimmer.crop(path, options, promise, reactContext);
85+
}
86+
87+
@ReactMethod
88+
private void loadFfmpeg() {
89+
Trimmer.loadFfmpeg(reactContext);
7890
}
7991
}

0 commit comments

Comments
 (0)