【问题标题】:Reducing app size by archiving ffmpeg/ffprobe library in Android通过在 Android 中归档 ffmpeg/ffprobe 库来减小应用程序大小
【发布时间】:2019-07-03 04:03:58
【问题描述】:

我在互联网上看到很多关于如何在 Android 中减小 FFmpeg/FFprobe 库大小的帖子。我也对这个话题进行了几个月的调查。所以我决定分享我的结论。我不是在谈论编译时设置和禁用/启用功能。当然,您可以这样做以获得更好的结果,但这篇文章不是关于此的。这是关于归档 FFmpeg/FFprobe 编译的文件,这将将您的库大小减少 85%

【问题讨论】:

    标签: android ffmpeg android-ffmpeg ffprobe android-ffprobe


    【解决方案1】:

    基于FFMpeg/FFprobe compiled for Android

    1. 将资产文件(ffmpeg && ffprobe) 压缩为任意存档格式,并替换为当前文件。

    2. 使用一些额外的代码,将存档文件复制到应用目录中asynctask

    3. 使用this library 提取存档(也支持rar 存档)。

    结果:79.8MB ==> 13.2MB

    提取时间:1 秒(Honor 8 Lite API 25/26。)。




    无压缩


    带压缩


    我正在使用此设置进行压缩:

    Windows 10 64 位,

    7-Zip应用程序

    .7z存档格式

    Ultra压缩等级,

    LZMA2压缩方式.

    您可以尝试其他压缩方法和存档格式来测试不同的结果。





    将存档复制/提取到应用目录所需的代码:


    将此添加到您要初始化复制过程的任何位置(我更喜欢在Application 类的onCreate 中)。

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

    上面的方法会处理这个函数:

    • 将存档从资产复制到应用目录。

    • 检查 CPU 架构模型。

    • 根据 CPU 型号提取存档。

    • 解压后删除存档。

    • 使文件可执行。

    • 如果设备支持 ffmpeg,则返回 true/false。

    完整代码:

    添加到 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";
                }
            }
    
        }
    
    
    }
    

    【讨论】:

    • 这对我不起作用,我不断收到onPostExecute: !ffmpegFile.exists()。我已将.7z 文件放在我的assets 文件夹中,并且完全按照您上面提到的方式进行操作。有什么原因无法正常工作吗?
    • @HB。仔细查看 With Compression 图像。如图所示命名并存档您的 .7z 文件(在 ffmpeg_arch.7z 文件中有两个文件夹:x86 arm ,其中有两个文件:ffmpeg.file , ffprobe.file ... 并将 .7z 文件直接放在里面资产文件夹)。你一定是做错了什么,因为在我在这里发布之前我已经多次测试了上述步骤。
    • 我做的和你做的一模一样——在armx86里面我有ffmpegffprobe。然后我压缩了armx86,最后得到了一个名为ffmpeg_arch.7z 的压缩文件,我将它放入了我的assets folder。然后我导入了你提到的 2 个库并创建了你提到的类(FFmpegArchiveUtil)。 .在onCreate 中,我的调用方式与您相同,当我运行它时,Log.d("onPostExecute", "!ffmpegFile.exists()"); 会被调用。
    • @HB。下载this并将其放入assets文件夹,然后再次运行您的项目,看看问题是否仍然存在,同时使用ide文件管理器打开您的应用目录,看看那里发生了什么。
    • 我不相信这会在 Android Q 上运行 - 你会遇到error=13, Permission denied。最好将ffmpeg重命名为lib..ffmpeg..so,放到jniLibs文件夹中。
    【解决方案2】:

    在 Gradle 文件中添加代码

    机器人{ ... aaptOptions { 忽略资产模式“!x86” } }

    Refer this link

    可能会有所帮助

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-09-01
      • 1970-01-01
      • 2022-10-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-22
      • 1970-01-01
      相关资源
      最近更新 更多