Skip to content

Commit 4d10f0c

Browse files
gevgasparyanShahen Hovhannisyan
authored andcommitted
feat(Compression): compress video on android (#69)
* check if format null properly * add compression method * compress video [android] * give video size according to orientation
1 parent dd3d14d commit 4d10f0c

File tree

9 files changed

+276
-68
lines changed

9 files changed

+276
-68
lines changed

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,14 @@ class App extends Component {
8383
.catch(console.warn);
8484
}
8585

86-
// iOS only
8786
compressVideo() {
8887
const options = {
8988
width: 720,
90-
endTime: 1280,
91-
bitrateMultiplier: 3,
92-
saveToCameraRoll: true, // default is false
93-
saveWithCurrentDate: true, // default is false
94-
minimumBitrate: 300000,
89+
height: 1280,
90+
bitrateMultiplier: 3, // iOS only
91+
saveToCameraRoll: true, // default is false, iOS only
92+
saveWithCurrentDate: true, // default is false, iOS only
93+
minimumBitrate: 300000, // iOS only
9594
removeAudio: true, // default is false
9695
};
9796
this.videoPlayerRef.compress(options)
@@ -183,7 +182,7 @@ export class App extends Component {
183182
## Roadmap
184183
1. [ ] Use FFMpeg instead of MP4Parser
185184
2. [ ] Add ability to add GLSL filters
186-
3. [ ] Android should be able to compress video
185+
3. [x] Android should be able to compress video
187186
4. [x] More processing options
188187
5. [ ] Create native trimmer component for Android
189188
6. [x] Provide Standalone API

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

Lines changed: 125 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package com.shahenlibrary.Trimmer;
2626

2727
import android.annotation.TargetApi;
28+
import android.content.Context;
2829
import android.graphics.Bitmap;
2930
import android.graphics.Matrix;
3031
import android.media.MediaMetadataRetriever;
@@ -39,9 +40,9 @@
3940
import com.facebook.react.bridge.ReadableMap;
4041
import com.facebook.react.bridge.WritableArray;
4142
import com.facebook.react.bridge.WritableMap;
42-
import com.facebook.react.uimanager.events.Event;
43+
import com.facebook.react.uimanager.ThemedReactContext;
4344
import com.shahenlibrary.Events.Events;
44-
import com.shahenlibrary.Events.EventsEnum;
45+
import com.shahenlibrary.interfaces.OnCompressVideoListener;
4546
import com.shahenlibrary.interfaces.OnTrimVideoListener;
4647
import com.shahenlibrary.utils.VideoEdit;
4748

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

9293
Log.d(TrimmerManager.REACT_PACKAGE, "getPreviewImages: \n\tduration: " + duration +
93-
"\n\twidth: " + width +
94-
"\n\theight: " + height +
95-
"\n\torientation: " + orientation +
96-
"\n\taspectRatio: " + aspectRatio +
97-
"\n\tresizeWidth: " + resizeWidth +
98-
"\n\tresizeHeight: " + resizeHeight
94+
"\n\twidth: " + width +
95+
"\n\theight: " + height +
96+
"\n\torientation: " + orientation +
97+
"\n\taspectRatio: " + aspectRatio +
98+
"\n\tresizeWidth: " + resizeWidth +
99+
"\n\tresizeHeight: " + resizeHeight
99100
);
100101

101102
Matrix mx = new Matrix();
@@ -141,6 +142,11 @@ public static void getVideoInfo(String path, Promise promise, ReactApplicationCo
141142
int width = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
142143
int height = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
143144
int orientation = Integer.parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION));
145+
if (orientation == 90 || orientation == 270) {
146+
width = width + height;
147+
height = width - height;
148+
width = width - height;
149+
}
144150

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

151157
event.putMap(Events.SIZE, size);
152-
event.putInt(Events.DURATION, duration);
158+
event.putInt(Events.DURATION, duration / 1000);
153159
event.putInt(Events.ORIENTATION, orientation);
154160

155161
promise.resolve(event);
@@ -215,7 +221,97 @@ public void cancelAction() {
215221
}
216222
}
217223

218-
static File createTempFile(String extension, final Promise promise, ReactApplicationContext ctx) {
224+
public static void compress(String source, ReadableMap options, final Promise promise, final OnCompressVideoListener cb, ThemedReactContext tctx, ReactApplicationContext rctx) {
225+
Context ctx = tctx != null ? tctx : rctx;
226+
227+
FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
228+
if (VideoEdit.shouldUseURI(source)) {
229+
retriever.setDataSource(ctx, Uri.parse(source));
230+
} else {
231+
retriever.setDataSource(source);
232+
}
233+
retriever.release();
234+
Log.d(LOG_TAG, "OPTIONS: " + options.toString());
235+
Double width = options.hasKey("width") ? options.getDouble("width") : null;
236+
Double height = options.hasKey("height") ? options.getDouble("height") : null;
237+
Double minimumBitrate = options.hasKey("minimumBitrate") ? options.getDouble("minimumBitrate") : null;
238+
Double bitrateMultiplier = options.hasKey("bitrateMultiplier") ? options.getDouble("bitrateMultiplier") : null;
239+
Boolean removeAudio = options.hasKey("removeAudio") ? options.getBoolean("removeAudio") : false;
240+
241+
final File tempFile = createTempFile("mp4", promise, ctx);
242+
243+
ArrayList<String> cmd = new ArrayList<String>();
244+
cmd.add("-y");
245+
cmd.add("-i");
246+
cmd.add(source);
247+
cmd.add("-c:v");
248+
cmd.add("libx264");
249+
if (width != null && height != null) {
250+
cmd.add("-vf");
251+
cmd.add("scale=" + Double.toString(width) + ":" + Double.toString(height));
252+
}
253+
254+
cmd.add("-preset");
255+
cmd.add("ultrafast");
256+
cmd.add("-pix_fmt");
257+
cmd.add("yuv420p");
258+
259+
if (removeAudio) {
260+
cmd.add("-an");
261+
}
262+
cmd.add(tempFile.getPath());
263+
264+
final String[] cmdToExec = cmd.toArray( new String[0] );
265+
266+
Log.d(LOG_TAG, Arrays.toString(cmdToExec));
267+
268+
try {
269+
FFmpeg.getInstance(ctx).execute(cmdToExec, new FFmpegExecuteResponseHandler() {
270+
271+
@Override
272+
public void onStart() {
273+
Log.d(LOG_TAG, "Compress: Start");
274+
}
275+
276+
@Override
277+
public void onProgress(String message) {
278+
}
279+
280+
@Override
281+
public void onFailure(String message) {
282+
if (cb != null) {
283+
cb.onError("compress error: failed. " + message);
284+
} else if (promise != null) {
285+
promise.reject("compress error: failed.", message);
286+
}
287+
}
288+
289+
@Override
290+
public void onSuccess(String message) {
291+
if (cb != null) {
292+
cb.onSuccess("file://" + tempFile.getPath());
293+
} else if (promise != null) {
294+
WritableMap event = Arguments.createMap();
295+
event.putString("source", "file://" + tempFile.getPath());
296+
promise.resolve(event);
297+
}
298+
}
299+
300+
@Override
301+
public void onFinish() {
302+
Log.d(LOG_TAG, "Compress: Finished");
303+
}
304+
});
305+
} catch (Exception e) {
306+
if (cb != null) {
307+
cb.onError("compress error. Command already running" + e.toString());
308+
} else if (promise != null) {
309+
promise.reject("compress error. Command already running", e.toString());
310+
}
311+
}
312+
}
313+
314+
static File createTempFile(String extension, final Promise promise, Context ctx) {
219315
UUID uuid = UUID.randomUUID();
220316
String imageName = uuid.toString() + "-screenshot";
221317

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

257353
WritableMap event = Arguments.createMap();
258354

259-
if ( format.equals(null) || format.equals("base64") ) {
355+
if ( format == null || (format != null && format.equals("base64")) ) {
260356
bmp.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
261357
byte[] byteArray = byteArrayOutputStream .toByteArray();
262358
String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);
@@ -406,27 +502,27 @@ public void onFinish() {
406502
public static void loadFfmpeg(ReactApplicationContext ctx){
407503
try {
408504
FFmpeg.getInstance(ctx).loadBinary(new FFmpegLoadBinaryResponseHandler() {
409-
@Override
410-
public void onStart() {
411-
Log.d(LOG_TAG, "load FFMPEG: onStart");
412-
}
505+
@Override
506+
public void onStart() {
507+
Log.d(LOG_TAG, "load FFMPEG: onStart");
508+
}
413509

414-
@Override
415-
public void onSuccess() {
416-
Log.d(LOG_TAG, "load FFMPEG: onSuccess");
417-
ffmpegLoaded = true;
418-
}
510+
@Override
511+
public void onSuccess() {
512+
Log.d(LOG_TAG, "load FFMPEG: onSuccess");
513+
ffmpegLoaded = true;
514+
}
419515

420-
@Override
421-
public void onFailure() {
422-
ffmpegLoaded = false;
423-
Log.d(LOG_TAG, "load FFMPEG: Failed to load ffmpeg");
424-
}
516+
@Override
517+
public void onFailure() {
518+
ffmpegLoaded = false;
519+
Log.d(LOG_TAG, "load FFMPEG: Failed to load ffmpeg");
520+
}
425521

426-
@Override
427-
public void onFinish() {
428-
Log.d(LOG_TAG, "load FFMPEG: onFinish");
429-
}
522+
@Override
523+
public void onFinish() {
524+
Log.d(LOG_TAG, "load FFMPEG: onFinish");
525+
}
430526
});
431527
} catch (Exception e){
432528
ffmpegLoaded = false;

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
import com.facebook.react.bridge.ReactContextBaseJavaModule;
3232
import com.facebook.react.bridge.ReactMethod;
3333
import com.facebook.react.bridge.ReadableMap;
34+
import com.facebook.react.common.MapBuilder;
35+
36+
import java.util.Map;
3437

3538
public class TrimmerManager extends ReactContextBaseJavaModule {
3639
static final String REACT_PACKAGE = "RNTrimmerManager";
@@ -65,17 +68,18 @@ public void trim(ReadableMap options, Promise promise) {
6568
Log.d(REACT_PACKAGE, options.toString());
6669
Trimmer.trim(options, promise);
6770
}
71+
6872
@ReactMethod
69-
public void compress(ReadableMap options, Promise promise) {
70-
Log.d(REACT_PACKAGE, "compress: not supported");
71-
promise.reject("not supported on android", "");
73+
public void compress(String path, ReadableMap options, Promise promise) {
74+
Log.d(REACT_PACKAGE, "compress video: " + options.toString());
75+
Trimmer.compress(path, options, promise, null, null, reactContext);
7276
}
7377

7478
@ReactMethod
7579
public void getPreviewImageAtPosition(ReadableMap options, Promise promise) {
7680
String source = options.getString("source");
77-
double sec = options.getDouble("second");
78-
String format = options.getString("format");
81+
double sec = options.hasKey("second") ? options.getDouble("second") : 0;
82+
String format = options.hasKey("format") ? options.getString("format") : null;
7983
Trimmer.getPreviewImageAtPosition(source, sec, format, promise, reactContext);
8084
}
8185

android/src/main/java/com/shahenlibrary/VideoPlayer/VideoPlayerView.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,17 @@
2929
import android.net.Uri;
3030
import android.os.Handler;
3131
import android.support.annotation.Nullable;
32-
import android.support.v4.app.ActivityCompat;
3332
import android.util.Base64;
3433
import android.util.Log;
3534
import android.widget.MediaController;
3635

3736
import com.facebook.react.bridge.Arguments;
3837
import com.facebook.react.bridge.LifecycleEventListener;
38+
import com.facebook.react.bridge.ReadableMap;
3939
import com.facebook.react.bridge.WritableMap;
4040
import com.facebook.react.uimanager.ThemedReactContext;
4141
import com.facebook.react.uimanager.events.RCTEventEmitter;
42+
import com.shahenlibrary.interfaces.OnCompressVideoListener;
4243
import com.shahenlibrary.interfaces.OnTrimVideoListener;
4344
import com.shahenlibrary.utils.VideoEdit;
4445
import com.yqritc.scalablevideoview.ScalableType;
@@ -55,8 +56,8 @@
5556
import wseemann.media.FFmpegMediaMetadataRetriever;
5657

5758
public class VideoPlayerView extends ScalableVideoView implements
58-
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener,
59-
MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl {
59+
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener,
60+
MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl {
6061

6162
private ThemedReactContext themedReactContext;
6263
private RCTEventEmitter eventEmitter;
@@ -362,6 +363,54 @@ public void cancelAction() {
362363
}
363364
}
364365

366+
public void compressMedia(ThemedReactContext ctx, ReadableMap options) {
367+
OnCompressVideoListener compressVideoListener = new OnCompressVideoListener() {
368+
@Override
369+
public void onError(String message) {
370+
Log.d(LOG_TAG, "Compress onError: " + message);
371+
WritableMap event = Arguments.createMap();
372+
event.putString(Events.ERROR_TRIM, message);
373+
eventEmitter.receiveEvent(getId(), EventsEnum.EVENT_GET_COMPRESSED_SOURCE.toString(), event);
374+
}
375+
376+
@Override
377+
public void onCompressStarted() {
378+
Log.d(LOG_TAG, "Compress Started");
379+
}
380+
381+
@Override
382+
public void onSuccess(String uri) {
383+
Log.d(LOG_TAG, "Compress: onSuccess");
384+
WritableMap event = Arguments.createMap();
385+
event.putString("source", uri.toString());
386+
eventEmitter.receiveEvent(getId(), EventsEnum.EVENT_GET_COMPRESSED_SOURCE.toString(), event);
387+
}
388+
389+
@Override
390+
public void cancelAction() {
391+
Log.d(LOG_TAG, "Compress cancel");
392+
}
393+
};
394+
395+
String[] dPath = mediaSource.split("/");
396+
StringBuilder builder = new StringBuilder();
397+
for (int i = 0; i < dPath.length; ++i) {
398+
if (i == dPath.length - 1) {
399+
continue;
400+
}
401+
builder.append(dPath[i]);
402+
builder.append(File.separator);
403+
}
404+
405+
try {
406+
VideoEdit.startCompress(mediaSource, compressVideoListener, ctx, options);
407+
} catch (IOException e) {
408+
compressVideoListener.onError(e.toString());
409+
e.printStackTrace();
410+
Log.d(LOG_TAG, "Error Compressing Video: " + e.toString());
411+
}
412+
}
413+
365414
@Override
366415
public boolean onError(MediaPlayer mp, int what, int extra) {
367416
return false;

0 commit comments

Comments
 (0)