bravobit / FFmpeg-Android

FFMpeg/FFprobe compiled for Android
https://bravobit.nl/
MIT License
739 stars 175 forks source link

Reducing ffmpeg library size by 85% #80

Open symphonyrecords opened 5 years ago

symphonyrecords commented 5 years ago

This is not an issue , it's more like an enhancement if you approve.

1. Compress asset files (ffmpeg && ffprobe) to any archive format, and replace with current files.

2. With some extra code, copy archive file in app directory with asynctask,

3. Extract archive with this library (Also supports rar archive).

Result: 79.8MB ==> 13.2MB

Extraction time: 1 Second (Honor 8 Lite API 25/26.).




Without Compression

Without Compression


With Compression

With Compression


I'm using this settings for compression:

Windows 10 64bit,

7-Zip Application,

.7z Archive format,

Ultra compression level,

LZMA2 compression method.

You can try other compression methods and archive formats to test different results.





Required codes to copy/extract archive to app directory:


Add this to anywhere you want to initialize copy process (I would prefer inside onCreate of Application class).

FFmpegArchiveUtil.initFFmpegBinary(this, new FFmpegArchiveUtil.FFmpegSupportCallback() {
    @Override
    public void isFFmpegSupported(boolean isSupported) {
        Log.d("isSupported: " , String.valueOf(isSupported));
    }
});

Above method will process this functions:

full code:

add to build.gradle :

android {
    defaultConfig {
        ndk {
            abiFilters 'x86' , 'arm64-v8a' , 'armeabi-v7a'
        }
    }
}

dependencies {
    implementation 'com.hzy:libp7zip:1.6.0'
    implementation 'commons-io:commons-io:2.6'
}

FFmpegArchiveUtil.java

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;

import com.hzy.libp7zip.P7ZipApi;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

@SuppressWarnings({"unused", "WeakerAccess"})
public class FFmpegArchiveUtil {

    public static final int VERSION = 17; // up this version when you add a new ffmpeg build
    public static final String KEY_PREF_VERSION = "ffmpeg_version";
    public static final String FFMPEG_ARCHIVE = "ffmpeg_arch.7z";
    public static final String FFMPEG_FILE_NAME = "ffmpeg";
    public static final String FFPROBE_FILE_NAME = "ffprobe";

    public static File getFFmpegArchive(Context context) {
        return new File(context.getFilesDir(), FFMPEG_ARCHIVE);
    }

    public static File getFFmpegFile(Context context) {
        return new File(context.getFilesDir(), FFMPEG_FILE_NAME);
    }

    public static File getFFprobe(Context context) {
        return new File(context.getFilesDir(), FFPROBE_FILE_NAME);
    }

    public interface FFmpegSupportCallback {
        void isFFmpegSupported(boolean isSupported);
    }

    private static class FFmpegArchiveCopyTask extends AsyncTask<Void, Void, Boolean> {
        InputStream stream;
        File ffmpegArchive;
        FFmpegSupportCallback callback;

        FFmpegArchiveCopyTask(InputStream stream, File file, FFmpegSupportCallback callback) {
            this.ffmpegArchive = file;
            this.stream = stream;
            this.callback = callback;
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            File ffmpegFile = new File(FilenameUtils.getFullPath(ffmpegArchive.getAbsolutePath()) + FFMPEG_FILE_NAME);
            if (ffmpegFile.exists()) {
                return true;
            } else {
                if (ffmpegArchive.exists()) {
                    return true;
                } else {
                    try {
                        FileUtils.copyToFile(stream, ffmpegArchive);
                        return true;
                    } catch (Exception e) {
                        return false;
                    }
                }
            }
        }

        @Override
        protected void onPostExecute(Boolean result) {
            try {
                FFmpegExtractorAsyncTask fFmpegExtractorAsyncTask = new FFmpegExtractorAsyncTask(stream, ffmpegArchive, callback);
                fFmpegExtractorAsyncTask.execute();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @SuppressWarnings("DanglingJavadoc")
    public static class FFmpegExtractorAsyncTask extends AsyncTask<Void, Void, Boolean> {

        InputStream stream;

        /**
         String archiveFormat =  {@link FFMPEG_ARCHIVE}
         */

        /**
         * The outPut path for copying archiveFormat
         * /data/user/0/com.symphonyrecords.mediacomp/files/
         */
        String bb;

        /**
         * FFmpeg archive file
         * /data/user/0/com.symphonyrecords.mediacomp/files/archiveFormat
         */
        File ffmpegArchive;

        /**
         * The main ffmpeg file
         * /data/user/0/com.symphonyrecords.mediacomp/files/ffmpeg
         */
        File ffmpegFile;

        FFmpegSupportCallback callback;

        FFmpegExtractorAsyncTask(InputStream stream, File file, FFmpegSupportCallback callback) {
            this.ffmpegArchive = file;
            this.stream = stream;
            this.callback = callback;
            bb = FilenameUtils.getFullPath(ffmpegArchive.getAbsolutePath());
            ffmpegFile = new File(bb + FFMPEG_FILE_NAME);
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            if (ffmpegFile.exists()) {
                return true;
            } else {
                if (ffmpegArchive.exists()) {
                    try {
                        String cmd = extractCmd(ffmpegArchive.getAbsolutePath(), bb);
                        P7ZipApi.executeCommand(cmd);
                        return true;
                    } catch (Throwable e) {
                        e.printStackTrace();
                        return false;
                    }
                } else {
                    try {
                        FileUtils.copyToFile(stream, ffmpegArchive);
                    } catch (Exception ignored) {
                    }
                }
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean isSuccess) {
            super.onPostExecute(isSuccess);
            if (isSuccess) {
                if (ffmpegFile.exists()) {
                    if (ffmpegArchive.exists()) {
                        deleteFile(ffmpegArchive.getAbsolutePath());
                    }
                    Log.d("onPostExecute", "ffmpegFile.exists()");
                    if (makeFileExecutable(ffmpegFile)) {
                        callback.isFFmpegSupported(true);
                        Log.d("onPostExecute", "makeFileExecutable Successful");
                    } else {
                        callback.isFFmpegSupported(false);
                        Log.d("onPostExecute", "makeFileExecutable Failed Again");
                    }
                } else {
                    callback.isFFmpegSupported(false);
                    Log.d("onPostExecute", "!ffmpegFile.exists()");
                }
                Log.d("onPostExecuteResult", "Successful");
            } else {
                Log.d("onPostExecute", "NotSuccessful");
                callback.isFFmpegSupported(false);
            }
        }
    }

    /**
     * Copying FFMPEG binary to application directory////////////
     */
    public static void initFFmpegBinary(Context context, FFmpegSupportCallback callback) {
        try {
            File f = getFFmpegFile(context);
            if (f.exists() && f.canExecute()) {
                callback.isFFmpegSupported(true);
            } else {
                extractFFMPEG(context, callback);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void extractFFMPEG(Context context, FFmpegSupportCallback callback) {
        if (CpuArchHelper.cpuNotSupported()) {
            callback.isFFmpegSupported(false);
            return;
        }
        // Copy Archive To App Dir
        SharedPreferences settings = context.getSharedPreferences("ffmpeg_prefs", Context.MODE_PRIVATE);
        int version = settings.getInt(KEY_PREF_VERSION, 0);

        // check if ffmpeg file exists
        if (getFFmpegFile(context).exists() && version >= VERSION) {
            callback.isFFmpegSupported(true);
        } else {
            try {
                Log.d("extractFFMPEG", "FFmpeg Binary does not exist, initializing copy process...");
                InputStream stream = context.getAssets().open(FFMPEG_ARCHIVE);
                FFmpegArchiveCopyTask fFmpegArchiveCopyTask = new FFmpegArchiveCopyTask(stream, getFFmpegArchive(context), callback);
                fFmpegArchiveCopyTask.execute();
                settings.edit().putInt(KEY_PREF_VERSION, VERSION).apply();
            } catch (Exception e) {
                Log.e("extractFFMPEG", "error while opening assets", e);
                callback.isFFmpegSupported(false);
            }
        }
    }

    public static boolean makeFileExecutable(File file) {
        if (!file.canExecute()) {
            // try to make executable
            try {
                try {
                    Runtime.getRuntime().exec("chmod -R 777 " + file.getAbsolutePath()).waitFor();
                } catch (InterruptedException e) {
                    Log.e("makeFileExecutable", "interrupted exception", e);
                    return false;
                } catch (IOException e) {
                    Log.e("makeFileExecutable", "io exception", e);
                    return false;
                }
                if (!file.canExecute()) {
                    if (!file.setExecutable(true)) {
                        Log.e("makeFileExecutable", "unable to make executable");
                        return false;
                    }
                }
            } catch (SecurityException e) {
                Log.e("makeFileExecutable", "security exception", e);
                return false;
            }
        }
        return file.canExecute();
    }

    private static void deleteFile(String fileName) {
        try {
            File file = new File(fileName);
            if (FileUtils.deleteQuietly(file))
                System.out.println(file.getName() + " is deleted!");
            else {
                if (file.delete()) {
                    System.out.println(file.getName() + " is deleted!");
                } else {
                    try {
                        FileUtils.forceDelete(file);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void createPath(File path) {
        try {
            FileUtils.forceMkdir(path);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static final String P7Z = "7z";
    /**
     * Command  Description
     * a    Add
     * b    Benchmark
     * d    Delete
     * e    Extract
     * h    Hash
     * i    Show information about supported formats
     * l    List
     * rn   Rename
     * t    Test
     * u    Update
     * x    eXtract with full paths
     */
    private static final String CMD_ADD = "a";
    private static final String CMD_BENCHMARK = "b";
    private static final String CMD_DELETE = "d";
    private static final String CMD_EXTRACT = "e";
    private static final String CMD_HASH = "h";
    private static final String CMD_INFO = "i";
    private static final String CMD_LIST = "l";
    private static final String CMD_RENAME = "rn";
    private static final String CMD_TEST = "t";
    private static final String CMD_UPDATE = "u";
    private static final String CMD_EXTRACT1 = "x";

    /**
     * Switch   Description
     * -i   Include filenames
     * -m   Set Compression Method
     * -o   Set Output directory
     * -p   Set Password
     * -t   Type of archive
     * -u   Update options
     * -x   Exclude filenames
     * -y   Assume Yes on all queries
     */
    private static final String SWH_INCLUDE = "-i";
    private static final String SWH_METHOD = "-m";
    private static final String SWH_OUTPUT = "-o";
    private static final String SWH_PASSWORD = "-p";
    private static final String SWH_TYPE = "-t";
    private static final String SWH_UPDATE = "-u";
    private static final String SWH_EXCLUDE = "-x";
    private static final String SWH_YES = "-y";

    public static String compressCmd(String filePath, String outPath, String type) {
        return String.format("7z a -t%s '%s' '%s'", type, outPath, filePath);
    }

    public static String extractCmd(String archivePath, String outPath) {
        return String.format("%s %s '%s' '%s%s' '%s' -aoa", P7Z, CMD_EXTRACT, archivePath, SWH_OUTPUT, outPath, CpuArchHelper.getCpuArchiveFolder());
    }
    //    public static String extractCmd(String archivePath, String outPath) {
    //        return String.format("%s %s '%s' '%s%s' -aoa", P7Z, CMD_EXTRACT1,archivePath, SWH_OUTPUT, outPath);
    //    }

    public static class CpuArchHelper {
        //// ---------  x86 Cpu ABI ----------- ////
        private static final String X86_CPU = "x86";
        private static final String X86_64_CPU = "x86_64";

        //// ---------  ARM Cpu ABI ----------- ////
        private static final String ARM_ABI = "armeabi";
        private static final String ARM_V7_CPU = "armeabi-v7a";
        private static final String ARM_64_CPU = "arm64-v8a";

        //// ---------  MIPS Cpu ABI ----------- ////
        private static final String MIPS_ABI = "mips";
        private static final String MIPS64_ABI = "mips64";

        public enum CpuArch {
            ARMv7, x86, NONE
        }

        public static CpuArch getCpuArch() {
            String cpu = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI;
            Log.d("Device_Cpu: ", cpu);
            switch (cpu) {
                case X86_CPU:
                case X86_64_CPU:
                    return CpuArch.x86;

                case ARM_ABI:
                case ARM_V7_CPU:
                case ARM_64_CPU:
                    return CpuArch.ARMv7;

                case MIPS_ABI:
                case MIPS64_ABI:
                    return CpuArch.NONE;

                default:
                    return CpuArch.NONE;
            }
        }

        public static boolean cpuNotSupported() {
            return getCpuArch() == CpuArch.NONE;
        }

        public static String getCpuArchiveFolder() {
            switch (getCpuArch()) {
                case ARMv7:
                    return "arm";
                case x86:
                    return "x86";
                case NONE:
                    return "arm";
                default:
                    return "arm";
            }
        }

    }

}
protectedMan commented 5 years ago

I don't understand what it means. Can I put some pictures?

symphonyrecords commented 5 years ago

@protectedMan @Savion1162336040 @abdelhady @Brianvdb I've added full details with all necessary codes. Result: 79.8MB ==> 13.2MB

protectedMan commented 5 years ago

The packaged APK is already a zip compression package, although the compression rate is not very high.

I tried to compare ffmpeg files with completely uncompressed ones using high compression rates, which is only about 2 Mb short after packaging.

symphonyrecords commented 5 years ago

@protectedMan I'm using both ffmpeg,ffprobe files for both x86,arm CPU arch. Signed APK size uncompressed ==>50.7MB and compressed ==> 23.1MB

dmkenza commented 4 years ago

It work well. But Bravobit dont add this imlementation in project yet.

I see libp7zip add extra MB in project too ~ +10MB. But total size reduced well.

I make fork with this implementation. https://github.com/dmkenza/FFmpeg-Android/blob/master/README.md

And I created library implementation 'com.github.dmkenza:FFmpeg-Android:1.1'

namdhis commented 3 years ago

It work well. But Bravobit dont add this imlementation in project yet.

I see libp7zip add extra MB in project too ~ +10MB. But total size reduced well.

I make fork with this implementation. https://github.com/dmkenza/FFmpeg-Android/blob/master/README.md

And I created library implementation 'com.github.dmkenza:FFmpeg-Android:1.1'

Hi @dmkenza. Your project doesn't work on Android 10, does it?

dmkenza commented 3 years ago

@namdhis Yes.

See https://github.com/tanersener/mobile-ffmpeg Only this way work on android 10. Generate own ffmpeg or use generated. Use abi split to reduce apk.

alhajsid commented 3 years ago

hello i am bit confuse i use implementation 'com.arthenica:mobile-ffmpeg-min-gpl:4.2.2.LTS' in my app and its ganerated files (.so one), and they are taking space, so if i have to remove implementation 'com.arthenica:mobile-ffmpeg-min-gpl:4.2.2.LTS' from my app? and where to place that .zip file please reply @symphonyrecords

vucong commented 3 years ago

I am new to ffmpeg for android. I want a little help, what format are FFmpeg file and FFprobe file? I don't know how to open it and view its contents. Do you have any documents to guide how to build file FFmpeg and file FFprobe?