Skip to content

Commit

Permalink
feat(android): Use FFPEG instead of Mp4parser, added new Crop method.…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
kesha-antonov authored and Shahen Hovhannisyan committed May 9, 2017
1 parent 451127d commit 77d5040
Show file tree
Hide file tree
Showing 12 changed files with 788 additions and 293 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,5 @@ jspm_packages

# Optional REPL history
.node_repl_history

.DS_Store
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ project(':react-native-video-processing').projectDir = new File(rootProject.proj
compile project(':react-native-video-processing')
```

6. Add this add the end of the file `android/app/build.gradle` (need it to download and compile ffmpeg lib):
```
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
```

#### [iOS]

1. In Xcode, click the "Add Files to <your-project-name>".
Expand Down Expand Up @@ -178,3 +187,4 @@ export class App extends Component {
4. [x] More processing options
5. [ ] Create native trimmer component for Android
6. [x] Provide Standalone API
7. [ ] Describe API methods with parameters in README
15 changes: 14 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

apply plugin: 'com.android.library'

android {
Expand All @@ -19,9 +18,23 @@ android {
}
}

configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}


dependencies {
compile 'com.facebook.react:react-native:0.20.+'
compile 'com.yqritc:android-scalablevideoview:1.0.4'
compile 'com.googlecode.mp4parser:isoparser:1.1.20'
compile 'com.github.wseemann:FFmpegMediaMetadataRetriever:1.0.14'
compile 'com.github.kesha-antonov:ffmpeg-android-java:e69ea470d69d271fafbd03d0a4b0ea67d6731ccc'
}

allprojects {
repositories {
mavenLocal()
jcenter()
maven { url "https://jitpack.io" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext
new VideoPlayerViewManager()
);
}
}
}
225 changes: 220 additions & 5 deletions android/src/main/java/com/shahenlibrary/Trimmer/Trimmer.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,22 @@

import wseemann.media.FFmpegMediaMetadataRetriever;

import java.util.UUID;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.ArrayList;

import com.github.hiteshsondhi88.libffmpeg.FFmpeg;
import com.github.hiteshsondhi88.libffmpeg.FFmpegExecuteResponseHandler;
import com.github.hiteshsondhi88.libffmpeg.FFmpegLoadBinaryResponseHandler;


public class Trimmer {

private static final String LOG_TAG = "RNTrimmerManager";

private static boolean ffmpegLoaded;

public static void getPreviewImages(String path, Promise promise, ReactApplicationContext ctx) {
FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
if (VideoEdit.shouldUseURI(path)) {
Expand Down Expand Up @@ -202,19 +214,222 @@ public void cancelAction() {
}
}

static void getPreviewAtPosition(String source, double sec, final Promise promise) {
static File createTempFile(String extension, final Promise promise, ReactApplicationContext ctx) {
UUID uuid = UUID.randomUUID();
String imageName = uuid.toString() + "-screenshot";

File cacheDir = ctx.getCacheDir();
File tempFile = null;
try {
tempFile = File.createTempFile(imageName, "." + extension, cacheDir);
} catch( IOException e ) {
promise.reject("Failed to create temp file", e.toString());
return null;
}

if (tempFile.exists()) {
tempFile.delete();
}

return tempFile;
}

static void getPreviewImageAtPosition(String source, double sec, String format, final Promise promise, ReactApplicationContext ctx) {
FFmpegMediaMetadataRetriever metadataRetriever = new FFmpegMediaMetadataRetriever();
FFmpegMediaMetadataRetriever.IN_PREFERRED_CONFIG = Bitmap.Config.ARGB_8888;
metadataRetriever.setDataSource(source);

Bitmap bmp = metadataRetriever.getFrameAtTime((long) (sec * 1000000));

// NOTE: FIX ROTATED BITMAP
int orientation = Integer.parseInt( metadataRetriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) );
metadataRetriever.release();

if ( orientation != 0 ) {
Matrix matrix = new Matrix();
matrix.postRotate(orientation);
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
}

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream .toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);

WritableMap event = Arguments.createMap();
event.putString("image", encoded);

if ( format.equals(null) || format.equals("base64") ) {
bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream .toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);

event.putString("image", encoded);
} else if ( format.equals("JPEG") ) {
bmp.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();

File tempFile = createTempFile("jpeg", promise, ctx);

try {
FileOutputStream fos = new FileOutputStream( tempFile.getPath() );

fos.write( byteArray );
fos.close();
} catch (java.io.IOException e) {
promise.reject("Failed to save image", e.toString());
return;
}

WritableMap imageMap = Arguments.createMap();
imageMap.putString("uri", "file://" + tempFile.getPath());

event.putMap("image", imageMap);
} else {
promise.reject("Wrong format error", "Wrong 'format'. Expected one of 'base64' or 'JPEG'.");
return;
}

promise.resolve(event);
}

static void crop(String source, ReadableMap options, final Promise promise, ReactApplicationContext ctx) {
int cropWidth = (int)( options.getDouble("cropWidth") );
int cropHeight = (int)( options.getDouble("cropHeight") );
int cropOffsetX = (int)( options.getDouble("cropOffsetX") );
int cropOffsetY = (int)( options.getDouble("cropOffsetY") );

FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
if (VideoEdit.shouldUseURI(source)) {
retriever.setDataSource(ctx, Uri.parse(source));
} else {
retriever.setDataSource(source);
}

int videoWidth = Integer.parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
int videoHeight = Integer.parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
retriever.release();

// NOTE: FFMpeg CROP NEED TO BE DEVIDED BY 2. OR YOU WILL SEE BLANK WHITE LINES FROM LEFT/RIGHT
while( cropWidth % 2 > 0 && cropWidth < videoWidth ) {
cropWidth += 1;
}
while( cropWidth % 2 > 0 && cropWidth > 0 ) {
cropWidth -= 1;
}
while( cropHeight % 2 > 0 && cropHeight < videoHeight ) {
cropHeight += 1;
}
while( cropHeight % 2 > 0 && cropHeight > 0 ) {
cropHeight -= 1;
}

// TODO: 1) ADD METHOD TO CHECK "IS FFMPEG LOADED".
// 2) CHECK IT HERE
// 3) EXPORT THAT METHOD TO "JS"

final File tempFile = createTempFile("mp4", promise, ctx);

ArrayList<String> cmd = new ArrayList<String>();
cmd.add("-y"); // NOTE: OVERWRITE OUTPUT FILE

String startTime = options.getString("startTime");
if ( !startTime.equals(null) && !startTime.equals("") ) {
cmd.add("-ss");
cmd.add(startTime);
}

// NOTE: INPUT FILE
cmd.add("-i");
cmd.add(source);

String endTime = options.getString("endTime");
if ( !endTime.equals(null) && !endTime.equals("") ) {
cmd.add("-to");
cmd.add(endTime);
}

cmd.add("-vf");
cmd.add("crop=" + Integer.toString(cropWidth) + ":" + Integer.toString(cropHeight) + ":" + Integer.toString(cropOffsetX) + ":" + Integer.toString(cropOffsetY));

cmd.add("-preset");
cmd.add("ultrafast");
// NOTE: DO NOT CONVERT AUDIO TO SAVE TIME
cmd.add("-c:a");
cmd.add("copy");
// NOTE: FLAG TO CONVER "AAC" AUDIO CODEC
cmd.add("-strict");
cmd.add("-2");
// NOTE: OUTPUT FILE
cmd.add(tempFile.getPath());

final String[] cmdToExec = cmd.toArray( new String[0] );

Log.d(LOG_TAG, Arrays.toString(cmdToExec));

try {
FFmpeg.getInstance(ctx).execute(cmdToExec, new FFmpegExecuteResponseHandler() {

@Override
public void onStart() {
Log.d(LOG_TAG, "crop: onStart");
}

@Override
public void onProgress(String message) {
Log.d(LOG_TAG, "crop: onProgress");
}

@Override
public void onFailure(String message) {
Log.d(LOG_TAG, "crop: onFailure");
promise.reject("Crop error: failed.", message);
}

@Override
public void onSuccess(String message) {
Log.d(LOG_TAG, "crop: onSuccess");
Log.d(LOG_TAG, message);

WritableMap event = Arguments.createMap();
event.putString("source", "file://" + tempFile.getPath());
promise.resolve(event);
}

@Override
public void onFinish() {
Log.d(LOG_TAG, "crop: onFinish");
}
});
} catch (Exception e) {
promise.reject("Crop error. Command already running", e.toString());
}
}

public static void loadFfmpeg(ReactApplicationContext ctx){
try {
FFmpeg.getInstance(ctx).loadBinary(new FFmpegLoadBinaryResponseHandler() {
@Override
public void onStart() {
Log.d(LOG_TAG, "load FFMPEG: onStart");
}

@Override
public void onSuccess() {
Log.d(LOG_TAG, "load FFMPEG: onSuccess");
ffmpegLoaded = true;
}

@Override
public void onFailure() {
ffmpegLoaded = false;
Log.d(LOG_TAG, "load FFMPEG: Failed to load ffmpeg");
}

@Override
public void onFinish() {
Log.d(LOG_TAG, "load FFMPEG: onFinish");
}
});
} catch (Exception e){
ffmpegLoaded = false;
Log.d("Failed to load ffmpeg", e.toString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class TrimmerManager extends ReactContextBaseJavaModule {
public TrimmerManager(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
loadFfmpeg();
}

@Override
Expand Down Expand Up @@ -74,6 +75,17 @@ public void compress(ReadableMap options, Promise promise) {
public void getPreviewImageAtPosition(ReadableMap options, Promise promise) {
String source = options.getString("source");
double sec = options.getDouble("second");
Trimmer.getPreviewAtPosition(source, sec, promise);
String format = options.getString("format");
Trimmer.getPreviewImageAtPosition(source, sec, format, promise, reactContext);
}

@ReactMethod
public void crop(String path, ReadableMap options, Promise promise) {
Trimmer.crop(path, options, promise, reactContext);
}

@ReactMethod
private void loadFfmpeg() {
Trimmer.loadFfmpeg(reactContext);
}
}
Loading

0 comments on commit 77d5040

Please sign in to comment.