在Android上使用FFmpeg压缩视频

2019-09-05 20:51:15

参考资料  在Android上使用FFmpeg压缩视频

前几天项目需要压缩视频,Github上找了许多库,要么就是太大,要么就是质量不高,其实我只需要压缩视频,最好的方案还是定制编译一个 FFmpeg 给 Android 用。

本项目使用 FFmpeg 和 libx264(一个第三方的视频编码器) 来编译出可以在 Android 上使用的动态库

一、下载源码

创建一个叫 FFmpegAndroid 的目录,下载 libx264 的源码ffmpeg源码,然后在 FFmpegAndroid文件夹下建立一个 bulid 文件夹,用于存放编译脚本和输出

--- FFmpegAndroid
 |-- ffmpeg |-- x264 |-- build

二、编译 FFmpeg

编译 x264 编码器

先在 build 文件夹下建立 setting.sh, 用于申明一些公用的环境变量,比如 $NDK$CPU...

setting.sh

# ndk 环境
NDK=$HOME/Library/Android/sdk/ndk-bundle
SYSROOT=$NDK/platforms/android-14/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
# cpu 架构平台,若要编译 x86 则指定 x86
CPU=armv7-a

然后建立 libx264 的编译脚本 build_x264.shlibx264 是一个开源的H.264编码器,据说是最好的视频有损编码器。ffmpeg 默认不自带,但是支持 x264 作为第三方编码器编译。

build_x264.sh

./config 内的# 注释必须在运行的时候去掉

#!/bin/bash

# 引入需要的环境变量
. setting.sh

# 输出下看看对不对,可以去掉,这里调试用
echo "use toolchain: $TOOLCHAIN"
echo "use system root: $SYSROOT"

# 输出文件的前缀,也就是指定最后静态库输出到那里
PREFIX=$(pwd)/lib/x264/$CPU
# 优化参数
OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "
ADDI_CFLAGS=""
ADDI_LDFLAGS=""

# 因为当前目录在 build 目录,需要切换到 x264 去执行 config
cd ../x264
function build_x264
{
./configure \
    --prefix=$PREFIX \
    # 不编译动态库
    --disable-shared \
    --disable-asm \
    # 编译静态库
    --enable-static \
    --enable-pic \
    --enable-strip \
    --host=arm-linux-androideabi \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    --sysroot=$SYSROOT \
    --extra-cflags="-Os -fpic $ADDI_CFLAGS $OPTIMIZE_CFLAGS" \
    --extra-ldflags="$ADDI_LDFLAGS" \
    $ADDITIONAL_CONFIGURE_FLAG
make clean
make -j4
make install
}

# 执行编译指令
build_x264

写完之后就可以编译 x264 库了,编译之前还有一点要注意的是,默认编译出来的文件后缀并不是 *.so,这 Android 是识别不了的,需要对 x264 源码里面的 config 做如下修改:

echo "SOSUFFIX=so" >> config.mak
echo "SONAME=libx264.so.$API" >> config.mak
echo "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak

修改成

echo "SOSUFFIX=so" >> config.mak
echo "SONAME=libx264-$API.so" >> config.mak
echo "SOFLAGS=-shared -Wl,-soname,\$(SONAME) $SOFLAGS" >> config.mak

别忘了给 build_x264.sh 和 setting.sh 赋予可执行权限 (chmod +x build_x264.sh setting.sh)

修改完后就可以执行脚本命令了

./build_x264.sh

等待一段时间后,build 文件夹目录下应该有个 lib 目录(build 脚本里面 prefix 指定的目录),里面存放了 x264 的静态库

这里为什么编译成静态库而不是动态库呢?静态库可以把内容编译到待会儿要编译 ffmpeg的so库里去,不需要单独加载 libx264.so 了,如果你硬要编译成动态库也可以,加载 ffmpeg.so 的时候加载 libx264.so 就可以

至此,x264编码器编译完毕

编译 FFmpeg

同样在 build 文件夹下建立编译脚本 build_ffmpeg.sh,编译 ffmpeg 比编译 x264 略微麻烦点,首先肯定不能全功能编译,那还不如直接去网上找一个编译好的,要自己定制哪些组件需要,哪些组件不需要

FFmpeg它主要含有以下几个核心库:

  • libavcodec-提供了更加全面的编解码实现的合集

  • libavformat-提供了更加全面的音视频容器格式的封装和解析以及所支持的协议

  • libavutil-提供了一些公共函数

  • libavfilter-提供音视频的过滤器,如视频加水印、音频变声等

  • libavdevice-提供支持众多设备数据的输入与输出,如读取摄像头数据、屏幕录制

  • libswresample,libavresample-提供音频的重采样工具

  • libswscale-提供对视频图像进行色彩转换、缩放以及像素格式转换,如图像的YUV转换

  • libpostproc-多媒体后处理器

如果不修改什么配置,直接编译的话,我发现 libavcodec.so 有 7.8MB,我可以在这方面下手,指定 decoder 和 encoder,因为我需要的是视频压缩,所以编码器(encoder)我就只需要 x264(视频编码) 和 aac(音频编码),至于解码器,挑几个常用的就可以了

查看编码器和解码器种类,可以通过 ./config --list-decoders 或 ./config --list-encoers 命令实现(ffmpeg目录下)
./config 内的# 注释必须在运行的时候去掉

#!/bin/bash

# 导入环境变量
. setting.sh

# 输出,调试用
echo "use toolchain: $TOOLCHAIN"
echo "use system root: $SYSROOT"

# x264库所在的位置,ffmpeg 需要链接 x264
LIB_DIR=$(pwd)/lib;

# ffmpeg编译输出前缀
PREFIX=$LIB_DIR/ffmpeg/$CPU
# x264的头文件地址
INC="$LIB_DIR/x264/$CPU/include"
# x264的静态库地址
LIB="$LIB_DIR/x264/$CPU/lib"
# 输出调试
echo "include dir: $INC"
echo "lib dir: $LIB"
# 编译优化参数
FF_EXTRA_CFLAGS="-march=$CPU -mfpu=vfpv3-d16 -mfloat-abi=softfp -mthumb" 
# 编译优化参数,-I$INC 指定 x264 头文件路径
FF_CFLAGS="-O3 -Wall -pipe \
-ffast-math \
-fstrict-aliasing -Werror=strict-aliasing \
-Wno-psabi -Wa,--noexecstack \
-DANDROID  \
-I$INC"

cd ../ffmpeg
function build_arm
{
./configure \
    # 这里需要启动生成动态库
    --enable-shared \
    # 静态库就不生成了
    --disable-static \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-ffserver \
    --disable-symver \
    # 禁用全部的编码
    --disable-encoders \
    # 启用 x264 这个库
    --enable-libx264 \
    # 启用 x264 编码
    --enable-encoder=libx264 \
    # 启用 aac 音频编码
    --enable-encoder=aac \
    # 启用几个图片编码,由于生成视频预览
    --enable-encoder=mjpeg \
    --enable-encoder=png \
    # 禁用全部的解码器
    --disable-decoders \
    # 启用几个常用的解码
    --enable-decoder=aac \
    --enable-decoder=aac_latm \
    --enable-decoder=h264 \
    --enable-decoder=mpeg4 \
    --enable-decoder=mjpeg \
    --enable-decoder=png \
    --disable-demuxers \
    --enable-demuxer=image2 \
    --enable-demuxer=h264 \
    --enable-demuxer=aac \
    --enable-demuxer=avi \
    --enable-demuxer=mpc \
    --enable-demuxer=mov \
    --disable-parsers \
    --enable-parser=aac \
    --enable-parser=ac3 \
    --enable-parser=h264 \
    # 这几个库应该需要,没怎么测试,反正很小就加上了
    --enable-avresample \
    --enable-small \
    --enable-avfilter \
    # 这两个是链接 x264 静态库需要
    --enable-gpl \
    --enable-yasm \
    # 编译输出前缀
    --prefix=$PREFIX \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    --target-os=linux \
    --arch=arm \
    --enable-cross-compile \
    --sysroot=$SYSROOT \
    --extra-cflags="$FF_CFLAGS $FF_EXTRA_CFLAGS" \
    # 指定 x264 静态库位置
    --extra-ldflags="-Wl,-L$LIB"
make clean
make -j16
make install
}

build_arm

这次编译不用静态库的原因是,静态库链接是有顺序要求的,这里模块太多,我也不知道哪个模块依赖哪个模块,所以直接上动态库

脚本写完后,就可以 run 了,编译时间有点久,可以学学我的某个同学,一编译就起来泡泡妹子,有说有笑。

编译完成后你的目录应该是下面那个样子:

--- FFmpegAndroid
    |-- ffmpeg    |-- x264    |-- build        |-- build_ffmpeg.sh        |-- build_x264.sh        |-- lib            |-- ffmpeg/armv7-a                |-- include (ffmpeg so库的头文件)
                |-- lib (ffmpeg so库)
                    |-- libavcodec-57.so                    |-- libavdevice-57.so                    |-- libavcodec-57.so                    |-- libavfilter-6.so                    |-- libavformat-57.so                    |-- libavresample-3.so                    |-- libavutil-55.so                    |-- libpostproc-54.so                    |-- libresample-2.so                    |-- libswscale-4.so            |-- x264 (x264的静态库和头文件)

后面的版本号不一样没关系,这由 ffmpeg 版本决定的

库编译完了,这些 so 库就是在 Android 可用的动态库,接下来就可以准备 JNI 编程了

三、在 Android 里使用 FFmpeg

前面已经把 FFmpeg 各个核心库编译出来了,但是我肯定不会在里面直接用核心库内的函数来用,ffmpeg 本来是一个在 pc 端的命令,命令里面可以填写各种参数,比如 ffmpeg -i a.mp4 -c:v x264 -c:a aac b.mp4,就是把 a.mp4 用 x264(视频)、aac(音频) 编码成 b.mp4

ffmpeg 是由 ffmpeg.c 编译出来的,想要在 Android 里面用 ffmpeg 命令,只要修改 ffmpeg.c 里面的 main 函数,比如修改成 int run_ffmpeg_command(int args, char **argv),然后用 JNI 暴露给 java 调用,就可以在 Android 使用 ffmpeg 命令了

在 FFmpegAndroid 建立一个 Android 工程,然后新建一个 ffmpeg 的 lib module
对于 NDK 开发,AndroidStudio 2.2 以后就有较好的支持,直接修改支持库的 build.gradle 文件

apply plugin: 'com.android.library'android {
    ...

    defaultConfig {
        ...
        // 启用 c++ 支持
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
            }
            ndk {
                abiFilters "armeabi-v7a"
            }
        }
    }

    ...

    // 指定 CMakeList 文件
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }}

这样 lib module 就支持 c++ 了,方便吧!比以前的 Android.mk 不知道方便多少

然后在模块的 src/main 下面新建一个 cpp 目录,用于存放 c++ 代码,从ffmpeg拷贝以下文件:

cmdutils_common_opts.h
cmdutils.c
cmdutils.h
config.h
ffmpeg_filter.c
ffmpeg_opt.c
ffmpeg-lib.c
ffmpeg.c
ffmpeg.h

然后在 CMakeList.txt 里面配置这些文件,好让 AndroidStudio 认识它们

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
            ffmpeg-lib

            # Sets the library as a shared library.
            SHARED

            # Provides a relative path to your source file(s).
             src/main/cpp/cmdutils.c
             src/main/cpp/ffmpeg.c
             src/main/cpp/ffmpeg_filter.c
             src/main/cpp/ffmpeg_opt.c
            # 此文件是用于暴露 ffmpeg.c 的 main 函数用
             src/main/cpp/ffmpeg-lib.c)

set(FFMPEG_LIB_DIR /Users/qigengxin/Documents/Github/FFmpegAndroid/build/lib/ffmpeg/armv7-a/lib)

add_library(
    avcodec
    SHARED
    IMPORTED
)
set_target_properties(
    avcodec
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavcodec-57.so
)

add_library(
    avdevice
    SHARED
    IMPORTED
)
set_target_properties(
    avdevice
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavdevice-57.so
)

add_library(
    avfilter
    SHARED
    IMPORTED
)
set_target_properties(
    avfilter
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavfilter-6.so
)

add_library(
    avformat
    SHARED
    IMPORTED
)
set_target_properties(
    avformat
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavformat-57.so
)

add_library(
    avresample
    SHARED
    IMPORTED
)
set_target_properties(
    avresample
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavresample-3.so
)

add_library(
    avutil
    SHARED
    IMPORTED
)
set_target_properties(
    avutil
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libavutil-55.so
)

add_library(
    postproc
    SHARED
    IMPORTED
)
set_target_properties(
    postproc
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libpostproc-54.so
)

add_library(
    swresample
    SHARED
    IMPORTED
)
set_target_properties(
    swresample
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libswresample-2.so
)

add_library(
    swscale
    SHARED
    IMPORTED
)
set_target_properties(
    swscale
    PROPERTIES IMPORTED_LOCATION
    ${FFMPEG_LIB_DIR}/libswscale-4.so
)

include_directories(
    ../../ffmpeg
)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
            log-lib

            # Specifies the name of the NDK library that
            # you want CMake to locate.
            log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
            ffmpeg-lib
            avcodec
            avutil
            avfilter
            swscale
            swresample
            avresample
            postproc
            avformat
            avdevice

            # Links the target library to the log library
            # included in the NDK.
            ${log-lib} )

刷新下 gradle,就可以写 c++ 代码了。先看下 ffmpeg.c 这个文件,原先的指令其实调用的就是 main 函数,我们先把 main 函数改成自己自定义的函数 run_ffmpeg_command:

int run_ffmpeg_command(int argc, char **argv){
    ...}

改了以后,我们就可以调用 run_ffmpeg_command 然后传入参数,相当于在 pc 执行 ffmpeg 命令。不过现在还不能执行,这是个坑点,仔细看 run_ffmpeg_command 函数,在程序结束的时候,或者中途出现错误的时候,都会调用 exit_program(int),这个函数:

int run_ffmpeg_command(int argc, char **argv){
    ...

    /* parse options and open all input/output files */
    ret = ffmpeg_parse_options(argc, argv);
    if (ret < 0){
        exit_program(1);
    }

    ...

    if (nb_output_files <= 0 && nb_input_files == 0) {
        show_usage();
        av_log(NULL, AV_LOG_WARNING, "Use -h to get full help or, even better, run 'man %s'\n", program_name);
        exit_program(1);
    }

    exit_program(received_nb_signals ? 255 : main_return_code);
    return main_return_code;}

exit_program(int) 函数是什么,跳过去看一下发现里面就是清理资源然后 exit(int),这里就要注意这个 exit 函数了,除非我们是多进程方式调用 run_ffmpeg_command,如果我们在 app 的进程调用,执行了 exit 就会结束 app 的进程!

这不是我想看到的,最好的方法是另开一个进程调用,但是这样就涉及到了进程间的通信问题,麻烦,不想写!反正只是跑一个压缩指令嘛,直接改 ffmpeg.c,首先把 exit(int) 函数给注释掉,然后返回一个 code,run_ffmpeg_command 函数里面只要涉及到 exit_program(int) 函数调用的地方都写成 return exit_program(int),不过要注意,有如下几个坑点:

修改 ffmpeg.c 坑点一

调试的时候发现 return exit_program(int); 语句并不会结束当前函数并返回,而是继续往下执行了,当时一脸楞逼,我艹!!这是什么鬼??为什么我 return 了没有用?找了半天后才发现是 exit_program(int) 这个函数声明的锅!看下面这个函数的声明:

/**
 * Wraps exit with a program-specific cleanup routine.
 */int exit_program(int ret) av_noreturn;

函数后面有个奇怪的 av_noreturn 声明,网上查了一下才知道,这个是给编译器的注解,这货的锅,去掉就好了。

修改 ffmpeg.c 坑点二

其实 exit_program(int) 这个函数不只是在 run_ffmpeg_command 里面调用,其它各种函数里面都有,如果都要修改的话必须一层一层的 return (C语言里面没有异常啊),很麻烦,但是如果没有改好的话就很容易 crash,这是个要解决的问题,首先 run_ffmpeg_command 里面的 exit_program 都要改成 return 方式

然后因为最终目的是压缩视频,参数集是固定的,所以不用考虑编码不支持,或参数匹配不到的情况,只需要考虑文件读写的问题,就是输入文件不存在的时候,或者输出路径不合法的时候,不能让程序异常退出,而是返回错误码,这个需要改 ffmpeg_opt.c 这个文件
ffmpeg_opt.c

static int open_files(OptionGroupList *l, const char *inout, int (*open_file)(OptionsContext*, const char*)){
    ...}static int open_input_file(OptionsContext *o, const char *filename){
    ...}static int open_outout_file(OptionsContext *o, const char *filename){
    ...}static int init_output_filter(OutputFilter *ofilter, OptionsContext *o, AVFormatContext *oc){
    ...}

目前我项目中就只改了这几个函数内的 exit_program,测试可行,也可以参考本项目的代码,链接在文末

最后就是暴露 run_ffmpeg_command 方法给 java 调用了,这个和普通的 JNI 编程一样,建一个 native 的方法,创建 cpp 代码。。。没啥东西,直接上代码

FFmpegNativeBridge

public class FFmpegNativeBridge {

    static {
        System.loadLibrary("ffmpeg-lib");
    }

    /**
     * 执行指令
     * @param command
     * @return 命令返回结果
     */
    public static native int runCommand(String[] command);}

ffmpeg-lib.c

#include <jni.h>#include "ffmpeg.h"JNIEXPORT jint JNICALLJava_org_voiddog_ffmpeg_FFmpegNativeBridge_runCommand(JNIEnv *env, jclass type,
                                                      jobjectArray command) {
    int argc = (*env)->GetArrayLength(env, command);
    char *argv[argc];
    jstring jsArray[argc];
    int i;
    for (i = 0; i < argc; i++) {
        jsArray[i] = (jstring) (*env)->GetObjectArrayElement(env, command, i);
        argv[i] = (char *) (*env)->GetStringUTFChars(env, jsArray[i], 0);
    }
    int ret = run_ffmpeg_command(argc,argv);
    for (i = 0; i < argc; ++i) {
        (*env)->ReleaseStringUTFChars(env, jsArray[i], argv[i]);
    }
    return ret;}

运行前先需要把 ffmpeg 编译出来的一堆 so 库放到 jniLibs 内,不然运行的时候会出现动态库无法加载的异常。最后就可以在 Android 内用 ffmpeg 的命令了:

 int ret = FFmpegNativeBridge.runCommand(new String[]{"ffmpeg",
                    "-i", "/storage/emulated/0/DCIM/Camera/VID_20170527_175421.mp4",
                    "-y",
                    "-c:v", "libx264",
                    "-c:a", "aac",
                    "-vf", "scale=480:-2",
                    "-preset", "ultrafast",
                    "-crf", "28",
                    "-b:a", "128k",
                    "/storage/emulated/0/Download/a.mp4"});

关于这些参数,可以去查 FFmpeg 的官网,本项目源码地址Github


  • 2019-12-04 10:48:18

    vue 项目资源文件 static 和 assets 不说区别直接使用?

    assets中资源会webpack构建压缩到你代码中,而static文件直接引用。 static 中长存放类包、插件等第三方的文件,assets里放属资源文件比如自己资源图片、css文件、js文件。 引入资源的方式static文件夹可以使用~/static/方式引入, assets文件夹可以使用 ~@/assets 方式引入

  • 2019-12-05 17:01:36

    Vue 结合 Axios 接口超时统一处理

    当网路慢的时候。又或者公司服务器不在内地的时候,接口数据请求不回来超时报错的情况相信大家肯定遇到过的,这里我把我公司项目请求超时的处理方法分享下,希望看过后有帮助。

  • 2019-12-05 17:13:40

    JS模板工具lodash.template的简单用法

    lodash是从underscore分支的一个项目,之前我写了一篇JS模板工具underscore.template的简单用法,lodash跟underscore很相似,这也简单介绍一下lodash的template方法。 先把underscore的文章中用过的代码贴过来,把underscore的js文件换成lodash的js,其他一字不改,然后我们试试:

  • 2019-12-06 10:47:29

    date-fns日期工具的使用方法详解

    isToday() 判断传入日期是否为今天 isYesterday() 判断传入日期是否为昨天 isTomorrow() 判断传入日期是否为 format() 日期格式化 addDays() 获得当前日期之后的日期 addHours() 获得当前时间n小时之后的时间点 addMinutes() 获得当前时间n分钟之后的时间 addMonths() 获得当前月之后n个月的月份 subDays() 获得当前时间之前n天的时间 subHours() 获得当前时间之前n小时的时间 subMinutes() 获得当前时间之前n分钟的时间 subMonths() 获得当前时间之前n个月的时间 differenceInYears() 获得两个时间相差的年份 differenceInWeeks() 获得两个时间相差的周数 differenceInDays() 获得两个时间相差的天数 differenceInHours() 获得两个时间相差的小时数 differenceInMinutes() 获得两个时间相差的分钟数

  • 2019-12-06 10:49:39

    npm 查看源 换源

    npm,cnpm,查看源,切换源,npm config set registry https://registry.npmjs.org

  • 2019-12-06 11:01:31

    npm发布包流程详解 有demo

    npm发布包步骤,以及踩过的坑(见红颜色标准): 1.注册npm账号,并完成Email认证(否则最后一步提交会报Email错误) 2.npm添加用户或登陆:npm adduser 或 npm login

  • 2019-12-06 13:16:18

    vue mixins组件复用的几种方式

    最近在做项目的时候,研究了mixins,此功能有妙处。用的时候有这样一个场景,页面的风格不同,但是执行的方法,和需要的数据非常的相似。我们是否要写两种组件呢?还是保留一个并且然后另个一并兼容另一个呢? 不管以上那种方式都不是很合理,因为组件写成2个,不仅麻烦而且维护麻烦;第二种虽然做了兼容但是页面逻辑造成混乱,必然不清晰;有没有好的方法,有那就是用vue的混合插件mixins。混合在Vue是为了提出相似的数据和功能,使代码易懂,简单、清晰。

  • 2019-12-06 13:26:30

    vue的mixins混入合并规则

    混入minxins:分发vue组件中可复用功能的灵活方式。混入对象可以包含任意组件选项。组件使用混入对象时,所有混入对象的选项将混入该组件本身的选项。