com.shahenlibrary.Trimmer.Trimmer.java Source code

Java tutorial

Introduction

Here is the source code for com.shahenlibrary.Trimmer.Trimmer.java

Source

/*
 * MIT License
 *
 * Copyright (c) 2017 Shahen Hovhannisyan.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.shahenlibrary.Trimmer;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.util.Base64;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.shahenlibrary.Events.Events;
import com.shahenlibrary.interfaces.OnCompressVideoListener;
import com.shahenlibrary.interfaces.OnTrimVideoListener;
import com.shahenlibrary.utils.VideoEdit;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import wseemann.media.FFmpegMediaMetadataRetriever;

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

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.util.Formatter;

public class Trimmer {

    private static final String LOG_TAG = "RNTrimmerManager";
    private static final String FFMPEG_FILE_NAME = "ffmpeg";
    private static final String FFMPEG_SHA1 = "77ae380db4bf56d011eca9ef9f20d397c0467aec";

    private static boolean ffmpegLoaded = false;
    private static final int DEFAULT_BUFFER_SIZE = 4096;
    private static final int END_OF_FILE = -1;

    private static class FfmpegCmdAsyncTaskParams {
        ArrayList<String> cmd;
        final String pathToProcessingFile;
        ReactApplicationContext ctx;
        final Promise promise;
        final String errorMessageTitle;
        final OnCompressVideoListener cb;

        FfmpegCmdAsyncTaskParams(ArrayList<String> cmd, final String pathToProcessingFile,
                ReactApplicationContext ctx, final Promise promise, final String errorMessageTitle,
                final OnCompressVideoListener cb) {
            this.cmd = cmd;
            this.pathToProcessingFile = pathToProcessingFile;
            this.ctx = ctx;
            this.promise = promise;
            this.errorMessageTitle = errorMessageTitle;
            this.cb = cb;
        }
    }

    private static class FfmpegCmdAsyncTask extends AsyncTask<FfmpegCmdAsyncTaskParams, Void, Void> {

        @Override
        protected Void doInBackground(FfmpegCmdAsyncTaskParams... params) {
            ArrayList<String> cmd = params[0].cmd;
            final String pathToProcessingFile = params[0].pathToProcessingFile;
            ReactApplicationContext ctx = params[0].ctx;
            final Promise promise = params[0].promise;
            final String errorMessageTitle = params[0].errorMessageTitle;
            final OnCompressVideoListener cb = params[0].cb;

            String errorMessageFromCmd = null;

            try {
                // NOTE: 3. EXECUTE "ffmpeg" COMMAND
                String ffmpegInDir = getFfmpegAbsolutePath(ctx);
                cmd.add(0, ffmpegInDir);
                Process p = new ProcessBuilder(cmd).start();

                BufferedReader input = getOutputFromProcess(p);
                String line = null;

                StringBuilder sInput = new StringBuilder();

                while ((line = input.readLine()) != null) {
                    Log.d(LOG_TAG, "processing ffmpeg");
                    System.out.println(sInput);
                    sInput.append(line);
                }
                input.close();

                int errorCode = p.waitFor();
                Log.d(LOG_TAG, "ffmpeg processing completed");

                if (errorCode != 0) {
                    BufferedReader error = getErrorFromProcess(p);
                    StringBuilder sError = new StringBuilder();

                    Log.d(LOG_TAG, "ffmpeg error code: " + errorCode);
                    while ((line = error.readLine()) != null) {
                        System.out.println(sError);
                        sError.append(line);
                    }
                    error.close();

                    errorMessageFromCmd = sError.toString();
                }
            } catch (Exception e) {
                errorMessageFromCmd = e.toString();
            }

            if (errorMessageFromCmd != null) {
                String errorMessage = errorMessageTitle + ": failed. " + errorMessageFromCmd;
                if (cb != null) {
                    cb.onError(errorMessage);
                } else if (promise != null) {
                    promise.reject(errorMessage);
                }
            } else {
                String filePath = "file://" + pathToProcessingFile;
                if (cb != null) {
                    cb.onSuccess(filePath);
                } else if (promise != null) {
                    WritableMap event = Arguments.createMap();
                    event.putString("source", filePath);
                    promise.resolve(event);
                }
            }

            return null;
        }

    }

    private static class LoadFfmpegAsyncTaskParams {
        ReactApplicationContext ctx;

        LoadFfmpegAsyncTaskParams(ReactApplicationContext ctx) {
            this.ctx = ctx;
        }
    }

    private static class LoadFfmpegAsyncTask extends AsyncTask<LoadFfmpegAsyncTaskParams, Void, Void> {

        @Override
        protected Void doInBackground(LoadFfmpegAsyncTaskParams... params) {
            ReactApplicationContext ctx = params[0].ctx;

            // NOTE: 1. COPY "ffmpeg" FROM ASSETS TO /data/data/com.myapp...
            String filesDir = getFilesDirAbsolutePath(ctx);

            // TODO: MAKE SURE THAT WHEN WE UPDATE FFMPEG AND USER UPDATES APP IT WILL LOAD NEW FFMPEG (IT MUST OVERWRITE OLD FFMPEG)
            try {
                File ffmpegFile = new File(filesDir, FFMPEG_FILE_NAME);
                if (!(ffmpegFile.exists() && getSha1FromFile(ffmpegFile).equalsIgnoreCase(FFMPEG_SHA1))) {
                    final FileOutputStream ffmpegStreamToDataDir = new FileOutputStream(ffmpegFile);
                    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];

                    int n;
                    InputStream ffmpegInAssets = ctx.getAssets()
                            .open("armeabi-v7a" + File.separator + FFMPEG_FILE_NAME);
                    while (END_OF_FILE != (n = ffmpegInAssets.read(buffer))) {
                        ffmpegStreamToDataDir.write(buffer, 0, n);
                    }

                    ffmpegStreamToDataDir.flush();
                    ffmpegStreamToDataDir.close();

                    ffmpegInAssets.close();
                }
            } catch (IOException e) {
                Log.d(LOG_TAG, "Failed to copy ffmpeg" + e.toString());
                ffmpegLoaded = false;
                return null;
            }

            String ffmpegInDir = getFfmpegAbsolutePath(ctx);

            // NOTE: 2. MAKE "ffmpeg" EXECUTABLE
            String[] cmdlineChmod = { "chmod", "700", ffmpegInDir };
            // TODO: 1. CHECK PERMISSIONS
            Process pChmod = null;
            try {
                pChmod = Runtime.getRuntime().exec(cmdlineChmod);
            } catch (IOException e) {
                Log.d(LOG_TAG, "Failed to make ffmpeg executable. Error in execution cmd. " + e.toString());
                ffmpegLoaded = false;
                return null;
            }

            try {
                pChmod.waitFor();
            } catch (InterruptedException e) {
                Log.d(LOG_TAG, "Failed to make ffmpeg executable. Error in wait cmd. " + e.toString());
                ffmpegLoaded = false;
                return null;
            }

            ffmpegLoaded = true;
            return null;
        }
    }

    public static void getPreviewImages(String path, Promise promise, ReactApplicationContext ctx) {
        FFmpegMediaMetadataRetriever retriever = new FFmpegMediaMetadataRetriever();
        try {
            if (VideoEdit.shouldUseURI(path)) {
                retriever.setDataSource(ctx, Uri.parse(path));
            } else {
                retriever.setDataSource(path);
            }

            WritableArray images = Arguments.createArray();
            int duration = Integer
                    .parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
            int width = Integer
                    .parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
            int height = Integer
                    .parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
            int orientation = Integer
                    .parseInt(retriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION));

            float aspectRatio = width / height;

            int resizeWidth = 200;
            int resizeHeight = Math.round(resizeWidth / aspectRatio);

            float scaleWidth = ((float) resizeWidth) / width;
            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);

            Matrix mx = new Matrix();

            mx.postScale(scaleWidth, scaleHeight);
            mx.postRotate(orientation - 360);

            for (int i = 0; i < duration; i += duration / 10) {
                Bitmap frame = retriever.getFrameAtTime(i * 1000);

                if (frame == null) {
                    continue;
                }
                Bitmap currBmp = Bitmap.createScaledBitmap(frame, resizeWidth, resizeHeight, false);

                Bitmap normalizedBmp = Bitmap.createBitmap(currBmp, 0, 0, resizeWidth, resizeHeight, mx, true);
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                normalizedBmp.compress(Bitmap.CompressFormat.PNG, 90, byteArrayOutputStream);
                byte[] byteArray = byteArrayOutputStream.toByteArray();
                String encoded = "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.DEFAULT);
                images.pushString(encoded);
            }

            WritableMap event = Arguments.createMap();

            event.putArray("images", images);

            promise.resolve(event);
        } finally {
            retriever.release();
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    public static void getVideoInfo(String path, Promise promise, ReactApplicationContext ctx) {
        FFmpegMediaMetadataRetriever mmr = new FFmpegMediaMetadataRetriever();
        try {
            if (VideoEdit.shouldUseURI(path)) {
                mmr.setDataSource(ctx, Uri.parse(path));
            } else {
                mmr.setDataSource(path);
            }

            int duration = Integer
                    .parseInt(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_DURATION));
            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));
            // METADATA_KEY_FRAMERATE returns a float or int or might not exist
            Integer frameRate = VideoEdit
                    .getIntFromString(mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_FRAMERATE));
            // METADATA_KEY_VARIANT_BITRATE returns a int or might not exist
            Integer bitrate = VideoEdit.getIntFromString(
                    mmr.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VARIANT_BITRATE));
            if (orientation == 90 || orientation == 270) {
                width = width + height;
                height = width - height;
                width = width - height;
            }

            WritableMap event = Arguments.createMap();
            WritableMap size = Arguments.createMap();

            size.putInt(Events.WIDTH, width);
            size.putInt(Events.HEIGHT, height);

            event.putMap(Events.SIZE, size);
            event.putInt(Events.DURATION, duration / 1000);
            event.putInt(Events.ORIENTATION, orientation);
            if (frameRate != null) {
                event.putInt(Events.FRAMERATE, frameRate);
            } else {
                event.putNull(Events.FRAMERATE);
            }
            if (bitrate != null) {
                event.putInt(Events.BITRATE, bitrate);
            } else {
                event.putNull(Events.BITRATE);
            }

            promise.resolve(event);
        } finally {
            mmr.release();
        }
    }

    static void trim(ReadableMap options, final Promise promise, ReactApplicationContext ctx) {
        String source = options.getString("source");
        String startTime = options.getString("startTime");
        String endTime = options.getString("endTime");

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

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

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

        // NOTE: PLACE ARGUMENTS FOR FFMPEG IN THIS ORDER:
        // 1. "-i" (INPUT FILE)
        // 2. "-ss" (START TIME)
        // 3. "-to" (END TIME) or "-t" (TRIM TIME)
        // OTHERWISE WE WILL LOSE ACCURACY AND WILL GET WRONG CLIPPED VIDEO

        cmd.add("-ss");
        cmd.add(startTime);

        cmd.add("-to");
        cmd.add(endTime);

        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());

        executeFfmpegCommand(cmd, tempFile.getPath(), ctx, promise, "Trim error", null);
    }

    private static ReadableMap formatWidthAndHeightForFfmpeg(int width, int height, int availableVideoWidth,
            int availableVideoHeight) {
        // NOTE: WIDTH/HEIGHT FOR FFMpeg NEED TO BE DEVIDED BY 2.
        // OR YOU WILL SEE BLANK WHITE LINES FROM LEFT/RIGHT (FOR CROP), OR CRASH FOR OTHER COMMANDS
        while (width % 2 > 0 && width < availableVideoWidth) {
            width += 1;
        }
        while (width % 2 > 0 && width > 0) {
            width -= 1;
        }
        while (height % 2 > 0 && height < availableVideoHeight) {
            height += 1;
        }
        while (height % 2 > 0 && height > 0) {
            height -= 1;
        }

        WritableMap sizes = Arguments.createMap();
        sizes.putInt("width", width);
        sizes.putInt("height", height);
        return sizes;
    }

    private static ReadableMap getVideoRequiredMetadata(String source, Context ctx) {
        Log.d(LOG_TAG, "getVideoRequiredMetadata: " + source);
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        try {
            if (VideoEdit.shouldUseURI(source)) {
                retriever.setDataSource(ctx, Uri.parse(source));
            } else {
                retriever.setDataSource(source);
            }

            int width = Integer
                    .parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
            int height = Integer
                    .parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
            int bitrate = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE));

            Log.d(LOG_TAG, "getVideoRequiredMetadata: " + Integer.toString(width));
            Log.d(LOG_TAG, "getVideoRequiredMetadata: " + Integer.toString(height));
            Log.d(LOG_TAG, "getVideoRequiredMetadata: " + Integer.toString(bitrate));

            WritableMap videoMetadata = Arguments.createMap();
            videoMetadata.putInt("width", width);
            videoMetadata.putInt("height", height);
            videoMetadata.putInt("bitrate", bitrate);
            return videoMetadata;
        } finally {
            retriever.release();
        }
    }

    public static void compress(String source, ReadableMap options, final Promise promise,
            final OnCompressVideoListener cb, ThemedReactContext tctx, ReactApplicationContext rctx) {
        Log.d(LOG_TAG, "OPTIONS: " + options.toString());

        Context ctx = tctx != null ? tctx : rctx;

        ReadableMap videoMetadata = getVideoRequiredMetadata(source, ctx);
        int videoWidth = videoMetadata.getInt("width");
        int videoHeight = videoMetadata.getInt("height");
        int videoBitrate = videoMetadata.getInt("bitrate");

        int width = options.hasKey("width") ? (int) (options.getDouble("width")) : 0;
        int height = options.hasKey("height") ? (int) (options.getDouble("height")) : 0;

        if (width != 0 && height != 0 && videoWidth != 0 && videoHeight != 0) {
            ReadableMap sizes = formatWidthAndHeightForFfmpeg(width, height, videoWidth, videoHeight);
            width = sizes.getInt("width");
            height = sizes.getInt("height");
        }

        Double minimumBitrate = options.hasKey("minimumBitrate") ? options.getDouble("minimumBitrate") : null;
        Double bitrateMultiplier = options.hasKey("bitrateMultiplier") ? options.getDouble("bitrateMultiplier")
                : 1.0;
        Boolean removeAudio = options.hasKey("removeAudio") ? options.getBoolean("removeAudio") : false;

        Double averageBitrate = videoBitrate / bitrateMultiplier;

        if (minimumBitrate != null) {
            if (averageBitrate < minimumBitrate) {
                averageBitrate = minimumBitrate;
            }
            if (videoBitrate < minimumBitrate) {
                averageBitrate = videoBitrate * 1.0;
            }
        }

        Log.d(LOG_TAG, "getVideoRequiredMetadata: averageBitrate - " + Double.toString(averageBitrate));

        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");
        cmd.add("-b:v");
        cmd.add(Double.toString(averageBitrate / 1000) + "K");
        cmd.add("-bufsize");
        cmd.add(Double.toString(averageBitrate / 2000) + "K");
        if (width != 0 && height != 0) {
            cmd.add("-vf");
            cmd.add("scale=" + Integer.toString(width) + ":" + Integer.toString(height));
        }

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

        if (removeAudio) {
            cmd.add("-an");
        }
        cmd.add(tempFile.getPath());

        executeFfmpegCommand(cmd, tempFile.getPath(), rctx, promise, "compress error", cb);
    }

    static File createTempFile(String extension, final Promise promise, Context 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) {
        Bitmap bmp = null;
        int orientation = 0;
        FFmpegMediaMetadataRetriever metadataRetriever = new FFmpegMediaMetadataRetriever();
        try {
            FFmpegMediaMetadataRetriever.IN_PREFERRED_CONFIG = Bitmap.Config.ARGB_8888;
            metadataRetriever.setDataSource(source);

            bmp = metadataRetriever.getFrameAtTime((long) (sec * 1000000));
            if (bmp == null) {
                promise.reject("Failed to get preview at requested position.");
                return;
            }

            // NOTE: FIX ROTATED BITMAP
            orientation = Integer.parseInt(
                    metadataRetriever.extractMetadata(FFmpegMediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION));
        } finally {
            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();

        WritableMap event = Arguments.createMap();

        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);

            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);
    }

    private static BufferedReader getOutputFromProcess(Process p) {
        return new BufferedReader(new InputStreamReader(p.getInputStream()));
    }

    private static BufferedReader getErrorFromProcess(Process p) {
        return new BufferedReader(new InputStreamReader(p.getErrorStream()));
    }

    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"));

        ReadableMap videoSizes = getVideoRequiredMetadata(source, ctx);
        int videoWidth = videoSizes.getInt("width");
        int videoHeight = videoSizes.getInt("height");

        ReadableMap sizes = formatWidthAndHeightForFfmpeg(cropWidth, cropHeight,
                // NOTE: MUST CHECK AGAINST "CROPPABLE" WIDTH/HEIGHT. NOT FULL WIDTH/HEIGHT
                videoWidth - cropOffsetX, videoHeight - cropOffsetY);
        cropWidth = sizes.getInt("width");
        cropHeight = sizes.getInt("height");

        // 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

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

        // NOTE: PLACE ARGUMENTS FOR FFMPEG IN THIS ORDER:
        // 1. "-i" (INPUT FILE)
        // 2. "-ss" (START TIME)
        // 3. "-to" (END TIME) or "-t" (TRIM TIME)
        // OTHERWISE WE WILL LOSE ACCURACY AND WILL GET WRONG CLIPPED VIDEO

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

        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());

        executeFfmpegCommand(cmd, tempFile.getPath(), ctx, promise, "Crop error", null);
    }

    static void boomerang(String source, final Promise promise, ReactApplicationContext ctx) {

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

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

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

        // NOTE: DO THE REVERSAL (credit: https://stackoverflow.com/a/42257863/6894670)
        cmd.add("-filter_complex");
        cmd.add("[0:v]reverse,fifo[r];[0:v][r] concat=n=2:v=1 [v]");

        cmd.add("-map");
        cmd.add("[v]");

        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());

        executeFfmpegCommand(cmd, tempFile.getPath(), ctx, promise, "Boomerang error", null);
    }

    static void reverse(String source, final Promise promise, ReactApplicationContext ctx) {

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

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

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

        // NOTE: DO THE REVERSAL (credit: https://video.stackexchange.com/a/17739)
        cmd.add("-vf");
        cmd.add("reverse");

        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());

        executeFfmpegCommand(cmd, tempFile.getPath(), ctx, promise, "Reverse error", null);
    }

    static private Void executeFfmpegCommand(@NonNull ArrayList<String> cmd,
            @NonNull final String pathToProcessingFile, @NonNull ReactApplicationContext ctx,
            @NonNull final Promise promise, @NonNull final String errorMessageTitle,
            @Nullable final OnCompressVideoListener cb) {
        FfmpegCmdAsyncTaskParams ffmpegCmdAsyncTaskParams = new FfmpegCmdAsyncTaskParams(cmd, pathToProcessingFile,
                ctx, promise, errorMessageTitle, cb);

        FfmpegCmdAsyncTask ffmpegCmdAsyncTask = new FfmpegCmdAsyncTask();
        ffmpegCmdAsyncTask.execute(ffmpegCmdAsyncTaskParams);

        return null;
    }

    private static String getFilesDirAbsolutePath(ReactApplicationContext ctx) {
        return ctx.getFilesDir().getAbsolutePath();
    }

    private static String getFfmpegAbsolutePath(ReactApplicationContext ctx) {
        return getFilesDirAbsolutePath(ctx) + File.separator + FFMPEG_FILE_NAME;
    }

    public static String getSha1FromFile(final File file) {
        MessageDigest messageDigest = null;
        try {
            messageDigest = MessageDigest.getInstance("SHA1");
        } catch (NoSuchAlgorithmException e) {
            Log.d(LOG_TAG, "Failed to load SHA1 Algorithm. " + e.toString());
            return "";
        }

        try {
            try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
                final byte[] buffer = new byte[1024];
                for (int read = 0; (read = is.read(buffer)) != -1;) {
                    messageDigest.update(buffer, 0, read);
                }
                is.close();
            }
        } catch (IOException e) {
            Log.d(LOG_TAG, "Failed to load SHA1 Algorithm. IOException. " + e.toString());
            return "";
        }

        try (Formatter f = new Formatter()) {
            for (final byte b : messageDigest.digest()) {
                f.format("%02x", b);
            }
            return f.toString();
        }
    }

    public static void loadFfmpeg(ReactApplicationContext ctx) {
        LoadFfmpegAsyncTaskParams loadFfmpegAsyncTaskParams = new LoadFfmpegAsyncTaskParams(ctx);

        LoadFfmpegAsyncTask loadFfmpegAsyncTask = new LoadFfmpegAsyncTask();
        loadFfmpegAsyncTask.execute(loadFfmpegAsyncTaskParams);

        // TODO: EXPOSE TO JS "isFfmpegLoaded" AND "isFfmpegLoading"

        return;
    }
}