Skip to content

Commit

Permalink
feat(Compression): compress video on android (#69)
Browse files Browse the repository at this point in the history
* check if format null properly

* add compression method

* compress video [android]

* give video size according to orientation
  • Loading branch information
gevgasparyan authored and Shahen Hovhannisyan committed Jun 7, 2017
1 parent dd3d14d commit 4d10f0c
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 68 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,14 @@ class App extends Component {
.catch(console.warn);
}

// iOS only
compressVideo() {
const options = {
width: 720,
endTime: 1280,
bitrateMultiplier: 3,
saveToCameraRoll: true, // default is false
saveWithCurrentDate: true, // default is false
minimumBitrate: 300000,
height: 1280,
bitrateMultiplier: 3, // iOS only
saveToCameraRoll: true, // default is false, iOS only
saveWithCurrentDate: true, // default is false, iOS only
minimumBitrate: 300000, // iOS only
removeAudio: true, // default is false
};
this.videoPlayerRef.compress(options)
Expand Down Expand Up @@ -183,7 +182,7 @@ export class App extends Component {
## Roadmap
1. [ ] Use FFMpeg instead of MP4Parser
2. [ ] Add ability to add GLSL filters
3. [ ] Android should be able to compress video
3. [x] Android should be able to compress video
4. [x] More processing options
5. [ ] Create native trimmer component for Android
6. [x] Provide Standalone API
Expand Down
154 changes: 125 additions & 29 deletions android/src/main/java/com/shahenlibrary/Trimmer/Trimmer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
package com.shahenlibrary.Trimmer;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.media.MediaMetadataRetriever;
Expand All @@ -39,9 +40,9 @@
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.ThemedReactContext;
import com.shahenlibrary.Events.Events;
import com.shahenlibrary.Events.EventsEnum;
import com.shahenlibrary.interfaces.OnCompressVideoListener;
import com.shahenlibrary.interfaces.OnTrimVideoListener;
import com.shahenlibrary.utils.VideoEdit;

Expand Down Expand Up @@ -90,12 +91,12 @@ public static void getPreviewImages(String path, Promise promise, ReactApplicati
float scaleHeight = ((float) resizeHeight) / height;

Log.d(TrimmerManager.REACT_PACKAGE, "getPreviewImages: \n\tduration: " + duration +
"\n\twidth: " + width +
"\n\theight: " + height +
"\n\torientation: " + orientation +
"\n\taspectRatio: " + aspectRatio +
"\n\tresizeWidth: " + resizeWidth +
"\n\tresizeHeight: " + resizeHeight
"\n\twidth: " + width +
"\n\theight: " + height +
"\n\torientation: " + orientation +
"\n\taspectRatio: " + aspectRatio +
"\n\tresizeWidth: " + resizeWidth +
"\n\tresizeHeight: " + resizeHeight
);

Matrix mx = new Matrix();
Expand Down Expand Up @@ -141,6 +142,11 @@ public static void getVideoInfo(String path, Promise promise, ReactApplicationCo
int width = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
int height = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
int orientation = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION));
if (orientation == 90 || orientation == 270) {
width = width + height;
height = width - height;
width = width - height;
}

WritableMap event = Arguments.createMap();
WritableMap size = Arguments.createMap();
Expand All @@ -149,7 +155,7 @@ public static void getVideoInfo(String path, Promise promise, ReactApplicationCo
size.putInt(Events.HEIGHT, height);

event.putMap(Events.SIZE, size);
event.putInt(Events.DURATION, duration);
event.putInt(Events.DURATION, duration / 1000);
event.putInt(Events.ORIENTATION, orientation);

promise.resolve(event);
Expand Down Expand Up @@ -215,7 +221,97 @@ public void cancelAction() {
}
}

static File createTempFile(String extension, final Promise promise, ReactApplicationContext ctx) {
public static void compress(String source, ReadableMap options, final Promise promise, final OnCompressVideoListener cb, ThemedReactContext tctx, ReactApplicationContext rctx) {
Context ctx = tctx != null ? tctx : rctx;

FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
if (VideoEdit.shouldUseURI(source)) {
retriever.setDataSource(ctx, Uri.parse(source));
} else {
retriever.setDataSource(source);
}
retriever.release();
Log.d(LOG_TAG, "OPTIONS: " + options.toString());
Double width = options.hasKey("width") ? options.getDouble("width") : null;
Double height = options.hasKey("height") ? options.getDouble("height") : null;
Double minimumBitrate = options.hasKey("minimumBitrate") ? options.getDouble("minimumBitrate") : null;
Double bitrateMultiplier = options.hasKey("bitrateMultiplier") ? options.getDouble("bitrateMultiplier") : null;
Boolean removeAudio = options.hasKey("removeAudio") ? options.getBoolean("removeAudio") : false;

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

ArrayList<String> cmd = new ArrayList<String>();
cmd.add("-y");
cmd.add("-i");
cmd.add(source);
cmd.add("-c:v");
cmd.add("libx264");
if (width != null && height != null) {
cmd.add("-vf");
cmd.add("scale=" + Double.toString(width) + ":" + Double.toString(height));
}

cmd.add("-preset");
cmd.add("ultrafast");
cmd.add("-pix_fmt");
cmd.add("yuv420p");

if (removeAudio) {
cmd.add("-an");
}
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, "Compress: Start");
}

@Override
public void onProgress(String message) {
}

@Override
public void onFailure(String message) {
if (cb != null) {
cb.onError("compress error: failed. " + message);
} else if (promise != null) {
promise.reject("compress error: failed.", message);
}
}

@Override
public void onSuccess(String message) {
if (cb != null) {
cb.onSuccess("file://" + tempFile.getPath());
} else if (promise != null) {
WritableMap event = Arguments.createMap();
event.putString("source", "file://" + tempFile.getPath());
promise.resolve(event);
}
}

@Override
public void onFinish() {
Log.d(LOG_TAG, "Compress: Finished");
}
});
} catch (Exception e) {
if (cb != null) {
cb.onError("compress error. Command already running" + e.toString());
} else if (promise != null) {
promise.reject("compress error. Command already running", e.toString());
}
}
}

static File createTempFile(String extension, final Promise promise, Context ctx) {
UUID uuid = UUID.randomUUID();
String imageName = uuid.toString() + "-screenshot";

Expand Down Expand Up @@ -256,7 +352,7 @@ static void getPreviewImageAtPosition(String source, double sec, String format,

WritableMap event = Arguments.createMap();

if ( format.equals(null) || format.equals("base64") ) {
if ( format == null || (format != null && format.equals("base64")) ) {
bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream .toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);
Expand Down Expand Up @@ -406,27 +502,27 @@ public void onFinish() {
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 onStart() {
Log.d(LOG_TAG, "load FFMPEG: onStart");
}

@Override
public void onSuccess() {
Log.d(LOG_TAG, "load FFMPEG: onSuccess");
ffmpegLoaded = true;
}
@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 onFailure() {
ffmpegLoaded = false;
Log.d(LOG_TAG, "load FFMPEG: Failed to load ffmpeg");
}

@Override
public void onFinish() {
Log.d(LOG_TAG, "load FFMPEG: onFinish");
}
@Override
public void onFinish() {
Log.d(LOG_TAG, "load FFMPEG: onFinish");
}
});
} catch (Exception e){
ffmpegLoaded = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;

import java.util.Map;

public class TrimmerManager extends ReactContextBaseJavaModule {
static final String REACT_PACKAGE = "RNTrimmerManager";
Expand Down Expand Up @@ -65,17 +68,18 @@ public void trim(ReadableMap options, Promise promise) {
Log.d(REACT_PACKAGE, options.toString());
Trimmer.trim(options, promise);
}

@ReactMethod
public void compress(ReadableMap options, Promise promise) {
Log.d(REACT_PACKAGE, "compress: not supported");
promise.reject("not supported on android", "");
public void compress(String path, ReadableMap options, Promise promise) {
Log.d(REACT_PACKAGE, "compress video: " + options.toString());
Trimmer.compress(path, options, promise, null, null, reactContext);
}

@ReactMethod
public void getPreviewImageAtPosition(ReadableMap options, Promise promise) {
String source = options.getString("source");
double sec = options.getDouble("second");
String format = options.getString("format");
double sec = options.hasKey("second") ? options.getDouble("second") : 0;
String format = options.hasKey("format") ? options.getString("format") : null;
Trimmer.getPreviewImageAtPosition(source, sec, format, promise, reactContext);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@
import android.net.Uri;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.util.Base64;
import android.util.Log;
import android.widget.MediaController;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.shahenlibrary.interfaces.OnCompressVideoListener;
import com.shahenlibrary.interfaces.OnTrimVideoListener;
import com.shahenlibrary.utils.VideoEdit;
import com.yqritc.scalablevideoview.ScalableType;
Expand All @@ -55,8 +56,8 @@
import wseemann.media.FFmpegMediaMetadataRetriever;

public class VideoPlayerView extends ScalableVideoView implements
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener,
MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl {
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener,
MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl {

private ThemedReactContext themedReactContext;
private RCTEventEmitter eventEmitter;
Expand Down Expand Up @@ -362,6 +363,54 @@ public void cancelAction() {
}
}

public void compressMedia(ThemedReactContext ctx, ReadableMap options) {
OnCompressVideoListener compressVideoListener = new OnCompressVideoListener() {
@Override
public void onError(String message) {
Log.d(LOG_TAG, "Compress onError: " + message);
WritableMap event = Arguments.createMap();
event.putString(Events.ERROR_TRIM, message);
eventEmitter.receiveEvent(getId(), EventsEnum.EVENT_GET_COMPRESSED_SOURCE.toString(), event);
}

@Override
public void onCompressStarted() {
Log.d(LOG_TAG, "Compress Started");
}

@Override
public void onSuccess(String uri) {
Log.d(LOG_TAG, "Compress: onSuccess");
WritableMap event = Arguments.createMap();
event.putString("source", uri.toString());
eventEmitter.receiveEvent(getId(), EventsEnum.EVENT_GET_COMPRESSED_SOURCE.toString(), event);
}

@Override
public void cancelAction() {
Log.d(LOG_TAG, "Compress cancel");
}
};

String[] dPath = mediaSource.split("/");
StringBuilder builder = new StringBuilder();
for (int i = 0; i < dPath.length; ++i) {
if (i == dPath.length - 1) {
continue;
}
builder.append(dPath[i]);
builder.append(File.separator);
}

try {
VideoEdit.startCompress(mediaSource, compressVideoListener, ctx, options);
} catch (IOException e) {
compressVideoListener.onError(e.toString());
e.printStackTrace();
Log.d(LOG_TAG, "Error Compressing Video: " + e.toString());
}
}

@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return false;
Expand Down
Loading

0 comments on commit 4d10f0c

Please sign in to comment.