跳至内容
返回

移植FFMPEG到安卓

发布于:  at  05:01 上午

移植FFmpeg到Android上

Ffmpegonandroidp1

因为Mix Music解码的需求,所以得选择合适的解码工具.尝试了4种解码方式,最后还是FFmpeg的效果最好

这是项目的GitHub地址,如果你的ffmpeg实在是编译不过去,可以去这里下载,然后再进行编译.

编译出ffmpeg的so文件

要编译FFmpeg,第一步就是先获取源码.直接去官网就可以下载. 解压后得到

└───ffmpeg
    ├───compat
    ├───doc
    ├───ffbuild
    ├───ffmpeg
    ├───fftools
    ├───libavcodec
    ├───libavdevice
    ├───libavfilter
    ├───libavformat
    ├───libavresample
    ├───libavutil
    ├───libpostproc
    ├───libswresample
    ├───libswscale
    ├───presets
    ├───tests
    └───tools

打开configure文件找到

SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

将内容修改为

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'

这是因为默认生成的文件名称中版本号位于最后,不符合Android命名规范.

在ffmpeg目录下创建build.sh文件 内容为


NDK=F:/android-ndk-r14b
## 指向你NDK的路径
SYSROOT=$NDK/platforms/android-21/arch-arm/
## 指定逻辑目录,按照自身需要进行修改
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64
## windows平台为windows-x86_64,linux为linux-x86_64
CPU=arm
PREFIX=$(pwd)/android/$CPU
ADDI_CFLAGS="-marm"
## build_one中,因为我只需要使用mp3的decode和pcm_s16le的encode,所以关闭了其他的decode和encode来实现压缩体积,集体配置参数可参考
## https://www.cnblogs.com/azraelly/archive/2012/12/31/2840541.html
function build_one
{
./configure \
--prefix=$PREFIX \
--enable-shared \
--disable-static \
--enable-asm \
--disable-doc \
--disable-gpl \
--enable-small \
--disable-encoders \
--disable-decoders \
--disable-ffmpeg \
--enable-encoder=pcm_s16le \
--enable-decoder=mp3 \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-doc \
--disable-symver \
--enable-jni \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--target-os=android \
--arch=arm \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
}
build_one

更加不幸的是.sh文件现在还不能运行,得先下载MinGW,记得build.sh文件不编辑的时候,不要挂在后台处在打开状态 下载好后,直接打开 勾选上如图所示的选项

Ffmpegonandroidp3

在左上角Installation菜单中点击Apply Changes,然后等待下载并自动安装(可能需要酸酸乳,因为网络质量不是很高)

下载完毕后,直接进度你的MinGW文件夹,找到msys文件夹并进入.双击msys.bat就会启动一个长的很像CMD的程序,至少在打开路径上都是使用cd命令

cd进入到之前build.sh的文件夹,输入./build.sh运行build.sh,它就会自动进行编译操作.如果遇到了各种奇怪问题,不要放弃,因为你的问题前人已经犯过,你只需要善用搜索引擎,踩着他们的脚印即可.

因为我在编译的时候也遇到了各种各样的问题,最后通过更换NDK版本解决了问题. 等待一段时间后,在ffmpeg目录下就会多出android文件夹,我们需要用到的so文件就在这个ffmpeg/android/arm/lib文件夹内

编译完成后我们得到了

include
lib
    libavcodec.so
    libavdevice.so
    libavfilter.so
    libavformat.so
    libavutil.so
    libpostproc.so
    libswresample.so
    libswscale.so

编译安卓可以调用的so文件

终于到了要调用FFmpeg的阶段了. 和常规NDK编译的需求一样,需要创建一个jni文件夹.把上一步得到的include文件夹和那些so文件复制进来.

还需要把位于ffmpeg根目录中的

创建Application.mk


APP_ABI := armeabi-v7a armeabi
APP_MODULES := libffmpegjni
APP_CFLAGS += -DSTDC_HEADERS

以及Android.mk

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE:= avcodec-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavcodec.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= avdevice-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavdevice.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= avfilter-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavfilter.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= avformat-prebuilt-armeabi
LOCAL_SRC_FILES:= prebuilt/armeabi/libavformat.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  avutil-prebuilt-armeabi
LOCAL_SRC_FILES := prebuilt/armeabi/libavutil.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample-prebuilt-armeabi
LOCAL_SRC_FILES := prebuilt/armeabi/libswresample.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale-prebuilt-armeabi
LOCAL_SRC_FILES := prebuilt/armeabi/libswscale.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := libffmpegjni

LOCAL_ARM_MODE := arm

LOCAL_SRC_FILES := ffmpegJni.c \
                   ffmpeg.c \
                   cmdutils.c \
                   ffmpeg_opt.c \
                   ffmpeg_filter.c \
                   ffmpeg_hw.c

LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz

LOCAL_SHARED_LIBRARIES:= avcodec-prebuilt-armeabi \
                         avdevice-prebuilt-armeabi \
                         avfilter-prebuilt-armeabi \
                         avformat-prebuilt-armeabi \
                         avutil-prebuilt-armeabi \
                         swresample-prebuilt-armeabi \
                         swscale-prebuilt-armeabi

LOCAL_C_INCLUDES += -L$(SYSROOT)/usr/include
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include

LOCAL_CFLAGS := -DUSE_ARM_CONFIG

include $(BUILD_SHARED_LIBRARY)

在ffmpeg.c中有些东西需要我们修改下,因为正常情况下ffmpeg执行一条命令,它就会自动退出,我们当然不希望这样.所以需要对他的推出操作进行处理. 打开ffmpeg.c,找到如下内容并修改

/* parse options and open all input/output files */
    ret = ffmpeg_parse_options(argc, argv);
    if (ret < 0){
        // exit_program(1);
        return 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);
        return 1;
    }

    /* file converter / grab */
    if (nb_output_files <= 0) {
        av_log(NULL, AV_LOG_FATAL, "At least one output file must be specified\n");
        //exit_program(1);
        return 1;
    }

    if (nb_input_files == 0) {
        av_log(NULL, AV_LOG_FATAL, "At least one input file must be specified\n");
        //exit_program(1);
        return 1;
    }

把下面的内容,添加到static void ffmpeg_cleanup(int ret)函数末尾

    ffmpeg_exited = 1;
    nb_filtergraphs = 0;
    nb_output_files = 0;
    nb_output_streams = 0;
    nb_input_files = 0;
    nb_input_streams = 0;

为了获取到ffmpeg的处理进度 修改log_callback_null函数内容为

static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl)
{
    static int print_prefix = 1;
    static int count;
    static char prev[1024];
    char line[1024];
    static int is_atty;
    av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);
    strcpy(prev, line);
    if (level <= AV_LOG_WARNING){
        XLOGE("%s", line);
    }else{
        XLOGD("%s", line);
        callJavaMethod(line);// 调用java方法,反馈进度
    }
}

并再main函数的开始处添加

av_log_set_callback(log_callback_null);

再打开cmdutils.c,对exit_program进行修改,别忘记修改头文件中的exit_program

int exit_program(int ret)
{
     return ret;
}

然后创建一个名为ffmpegJni.c的文件和它的头文件ffmpegJni.h

##include "logjni.h"
##include "ffmpegJni.h"
##include <stdlib.h>
##include <stdbool.h>

int main(int argc, char **argv);
static JavaVM *jvm = NULL;
static jclass m_clazz = NULL;
static jclass m_jobj = NULL;
static JNIEnv *m_env = NULL;
// 注意这里的Java_com_chaosgoo_ffmpegproject_ffmpegJni_main,请按照你的项目名称进行修改
JNIEXPORT jint JNICALL Java_com_chaosgoo_ffmpegproject_ffmpegJni_main(JNIEnv *env, jclass obj, jobjectArray commands){
    (*env)->GetJavaVM(env, &jvm); //获取JVM虚拟机
    jclass clazz = (*env)->GetObjectClass(env,obj);  //获取调用此方法的java类
    m_jobj = obj;
    m_clazz = (*env)->NewGlobalRef(env, clazz); //将这个类赋值给m_clazz
    m_env = env; //复制到全局变量m_env
    int argc = (*env)->GetArrayLength(env, commands);
    char *argv[argc];
    int i;
    int result = 0;
    for (i = 0; i < argc; i++)
    {
        jstring js = (jstring)(*env)->GetObjectArrayElement(env, commands, i);
        argv[i] = (char *)(*env)->GetStringUTFChars(env, js, 0);
    }
    LOGD("----------begin---------");
    int ret = main(argc, argv);
    ffmpegJniDone(1);
    return ret;
}


void callJavaMethod(char *ret)
{
    int ss = 0;
    char *q = strstr(ret, "time=");
    if (q != NULL)
    {
        //LOGE("遇到time=");
        char str[14] = {0};
        strncpy(str, q, 13);
        int h = (str[5] - '0') * 10 + (str[6] - '0');
        int m = (str[8] - '0') * 10 + (str[9] - '0');
        int s = (str[11] - '0') * 10 + (str[12] - '0');
        ss = s + m * 60 + h * 60 * 60;
    }
    else
    {
        return;
    }

    if (m_clazz == NULL)
    {
        LOGE("---------------m_clazz isNULL---------------");
        return;
    }
    //获取方法ID (I)V指的是方法签名 通过javap -s -public FFmpegCmd 命令生成
    jmethodID methodID = (*m_env)->GetMethodID(m_env, m_clazz, "onProgress", "(I)V");
    if (methodID == NULL)
    {
        LOGE("---------------methodID isNULL---------------");
        return;
    }
     LOGE("---------------Call Method---------------");
    //调用该java方法
    (*m_env)->CallVoidMethod(m_env,m_jobj, methodID, ss);
}

void ffmpegJniDone(int i){
    jmethodID methodID = (*m_env)->GetMethodID(m_env, m_clazz, "onFinish", "(I)V");
    (*m_env)->CallVoidMethod(m_env,m_jobj, methodID, i);
    // 完成任务后的调用java方法,告知其已经完成
}

logjni.h是为了将ffmpeg自身的输出变为Android Log输出的一个头文件,内容为

##ifdef ANDROID
##include <android/log.h>
##ifndef LOG_TAG
##define  MY_TAG   "MYTAG"
##define  AV_TAG   "AVLOG"
##endif
##define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, MY_TAG, format, ##__VA_ARGS__)
##define LOGD(format, ...)  __android_log_print(ANDROID_LOG_DEBUG,  MY_TAG, format, ##__VA_ARGS__)
##define  XLOGD(...)  __android_log_print(ANDROID_LOG_INFO,AV_TAG,__VA_ARGS__)
##define  XLOGE(...)  __android_log_print(ANDROID_LOG_ERROR,AV_TAG,__VA_ARGS__)
##else
##define LOGE(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
##define LOGD(format, ...)  printf(MY_TAG format "\n", ##__VA_ARGS__)
##define XLOGE(format, ...)  fprintf(stdout, AV_TAG ": " format "\n", ##__VA_ARGS__)
##define XLOGI(format, ...)  fprintf(stderr, AV_TAG ": " format "\n", ##__VA_ARGS__)
##endif

到这一步就可以执行ndk-build命令进行编译了. cmd 进入之前创建的jni文件夹,输入ndk-build (别忘记配置好ndk的环境变量)后,就会自动的进行编译操作,

如依然提示各种缺少文件,可以到之前解压出的ffmpeg文件夹里面去寻找到他们,然后复制到对应的文件夹中 最后,编译完成后,就会多出一个libffmpegjni.so文件 接下来就是Android端进行调用了 创建一个FFmpegJni类

public class FFmpegJni {
    private  float totalDecodeTime = 0f;
    private  ProgressBar progressBar;

    private String fileInPath;
    private String fileOutPath;

    MediaPlayer mediaPlayer = new MediaPlayer();

    public FFmpegJni(String inPath, String outPath, ProgressBar progressBar){
        fileInPath = inPath;
        fileOutPath = outPath;
        this.progressBar = progressBar;
    }
    // 初始化相关设定
    public void init(){
        try{
            mediaPlayer.setDataSource(fileInPath);
            mediaPlayer.prepare();
            totalDecodeTime = mediaPlayer.getDuration()/1000;
            Log.d("ffmpeg", "init: "+totalDecodeTime);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    //执行解码操作
    public void decode(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                String[] cmd = {"ffmpeg", "-i",fileInPath, "-f", "s16be" ,"-ar" ,"44100" ,"-acodec","pcm_s16le", fileOutPath};
                main(cmd);
            }
        }).start();
    }

    // 更新进度条
    public void onProgress(int second) {
        Log.d("ffmpeg", "onProgress: "+ ((second/this.totalDecodeTime) * 100));
        progressBar.setProgress((int)((second/this.totalDecodeTime) * 100));
    }


    // NDK 函数
    public native int main(String[] commands);
    // 导入so文件
    static {
        System.loadLibrary("avutil");
        System.loadLibrary("swresample");
        System.loadLibrary("avcodec");
        System.loadLibrary("avformat");
        System.loadLibrary("swscale");
        System.loadLibrary("avfilter");
        System.loadLibrary("avdevice");
        System.loadLibrary("ffmpegjni");
    }
}

然后再mainacitivity中调用即可,FFmpegJni的main函数接收命令.

参考资料


在以下平台分享此文章: