移植FFmpeg到Android平台
由于在ubuntu编译出来的FFmpeg动态链接库并不能直接在Android平台使用
所以我们需要使用 ndk 进行进一步的交叉编译
使得编译出来的动态链接库和我们的Android平台CPU架构相对应
1. 编译方式
本文采用 ndk + JNI + Android.mk + Application.mk 的方式进行编译
2. 编写调用C语言函数的java类
阅读本文前可以先了解JNI的相关内容来帮助理解
由于java并不能直接调用C语言的函数
所以我们需要编写一个java类和函数
来与我们希望调用的C语言函数一一对应
package com.jniutil;
public class FFmpegJni {
public static native int run(String[] commands) ;
}
3. 生成java类对应的C语言函数头文件
我们上面的 java 类包名为 com.jniutil
故切换到 com 包的上一级目录下运行命令
javah -classpath . com.jniutil.FFmpegJni
运行结束后我们可以在 com 包的同级目录下找到生成的C语言头文件
com_jniutil_FFmpegJni.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jniutil_FFmpegJni */
#ifndef _Included_com_jniutil_FFmpegJni
#define _Included_com_jniutil_FFmpegJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_jniutil_FFmpegJni
* Method: run
* Signature: ([Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_com_jniutil_FFmpegJni_run
(JNIEnv *, jclass, jobjectArray);
#ifdef __cplusplus
}
#endif
#endif
我们可以发现生成的文件名和函数名都特别有规律
都是在java的不同包名类名函数名之间加上下划线拼接而成的
4. 编写C语言实现函数
为了方便进行编译工作
我们可以在 main 文件夹下新建一个 jni_cmd 文件夹
有关 ffmpeg 编译的文件都会放在该文件夹下
故我们将刚刚生成的 com_jniutil_FFmpegJni.h 文件剪切到 jni_cmd 文件夹下
并创建一个 com_jniutil_FFmpegJni.c 文件
文件内容如下
// 导入一个 android_log.h 用于将FFmpeg的输出内容打印到我们的控制台中
// 这个文件我们等一下会实现
#include "android_log.h"
// 导入该C语言文件的头文件
#include "com_jniutil_FFmpegJni.h"
// 导入头文件 ffmpeg.h
// 这个文件在 ffmpeg 源码中的 fftools 文件夹下
// 用于我们调用 ffmpeg 中的函数
#include "ffmpeg.h"
JNIEXPORT jint JNICALL Java_com_jniutil_FFmpegJni_run
(JNIEnv *env, jclass obj, jobjectArray commands){
// 获取传入的String数组长度
int argc = (*env)->GetArrayLength(env, commands);
// 根据String数组长度创建Char数组
char *argv[argc];
int i;
// 复制String数组的内容到char数组
for (i = 0; i < argc; i++) {
jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
}
// LOGD是我们在android_log.h中定义的一个宏
// 对应一个具体的打印函数输出到控制台
LOGD("----------begin----------");
// 调用ffmpeg中的main函数并传入我们的命令行参数
// 此处实现了用命令行调用ffmpeg
// 可以不用手动调用ffmpeg复杂的api方法
return main(argc, argv);
}
5. 添加编译依赖文件
-
实现 android_log.h 内容如下
#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
-
从FFmpeg源码中复制以下文件
ffmpeg.c ffmpeg.h
cmdutils.c cmdutils.h
ffmpeg_filter.c
ffmpeg_opt.c
ffmpeg_hw.c (该文件可能视版本而定不一定要添加
-
复制FFmpeg源码
拷贝整个FFmpeg源码文件夹到 jni_cmd 目录下
-
复制ubuntu编译生成动态链接库文件夹
拷贝动态链接库文件夹到 jni_cmd 目录下
6. 修改ffmpeg.c文件
-
添加日志打印头文件
#include "android_log.h"
-
实现log_callback_null方法
这个方法在ffmpeg.c中原本就有
只是没有实现,是一个空方法
我们实现之后可以将ffmpeg内部的log输出到控制台上方便进行调试
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);
}
}
-
设置回调方法为log_callback_null
在main函数开头添加回调设置
int main(int argc, char **argv)
{
av_log_set_callback(log_callback_null);
int i, ret;
......
-
修改ffmpeg_cleanup方法
由于在Android中执行一条ffmpeg命令后并不需要退出程序
所以需要我们在每执行一条命令后都对变量进行初始化以便于重复使用
故在ffmpeg_cleanup方法结尾添加以下代码
filtergraphs = NULL;
nb_filtergraphs = 0;
output_files = NULL;
nb_output_files = 0;
output_streams = NULL;
nb_output_streams = 0;
input_files = NULL;
nb_input_files = 0;
input_streams = NULL;
nb_input_streams = 0;
-
调用ffmpeg_cleanup方法
在main函数的最后手动调用一次ffmpeg_cleanup来进行初始化
......
ffmpeg_cleanup(0);
return main_return_code;
}
7. 修改cmdutils.c文件
// exit_program原方法
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
exit(ret);
}
// 修改exit_program方法如下
// 使其在执行完一条命令后不会主动退出程序
int exit_program(int ret)
{
return ret;
}
同时我们也需要更新一下头文件cmdutils.h的函数定义
// 原定义
void exit_program(int ret) av_noreturn;
// 修改为
int exit_program(int ret);
8. 编写Application.mk文件
# 希望编译得到的运行平台cpu架构
# 我使用的是小米6骁龙835
APP_ABI := armeabi-v7a
# 最低的android版本
APP_PLATFORM := android-20
9. 编写Android.mk文件
LOCAL_PATH:= $(call my-dir)
# 定义两个变量记录了动态链接库 include 和 lib 文件位置
INCLUDE_PATH := ./simplefflib/include
FFMPEG_LIB_PATH := ./simplefflib/lib
# 编译libavcodec-57.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavcodec-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libavformat-57.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavformat-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libswscale-4.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswscale-4.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libavutil-55.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavutil-55.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libavfilter-6.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libavfilter
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavfilter-6.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libswresample-2.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libswresample
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libswresample-2.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libpostproc-54.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libpostproc
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libpostproc-54.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译libavdevice-57.so
include $(CLEAR_VARS)
LOCAL_MODULE:= libavdevice
LOCAL_SRC_FILES:= $(FFMPEG_LIB_PATH)/libavdevice-57.so
LOCAL_EXPORT_C_INCLUDES := $(INCLUDE_PATH)
include $(PREBUILT_SHARED_LIBRARY)
# 编译我们自己编写的C语言文件com_jniutil_FFmpegJni.c
include $(CLEAR_VARS)
# 希望编译的得到的模块名称
# 此处我们最后会得到一个 libffmpeg-cmd.so
LOCAL_MODULE := ffmpeg-cmd
# 编译时所依赖的文件需要一起编译
LOCAL_SRC_FILES := com_jniutil_FFmpegJni.c \
cmdutils.c \
ffmpeg.c \
ffmpeg_opt.c \
ffmpeg_filter.c \
ffmpeg_hw.c
# 依赖的源码
LOCAL_C_INCLUDES := ./ffmpeg-3.4.7
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice
include $(BUILD_SHARED_LIBRARY)
10. 最后生成的文件夹结构如下
其中 libs 和 obj 文件夹是最后编译得到的文件,未编译暂时不存在
simplefflib 是ubuntu编译得到的动态链接库,包括 include、lib、share 三个文件夹
11. ndk编译
在 jni_cmd 目录下运行命令
需要配置ndk环境才可以直接使用ndk-build命令
得到的libs文件夹就是我们编译完成可以使用的动态链接库
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk NDK_APPLICATION_MK=./Application.mk
12. 使用动态链接库
在build.gradle中设置我们依赖的动态链接库目录路径
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDirs = ['src/main/jni_cmd/libs']
}
}
有必要的话有也可以设置一下app限定运行的cpu架构
defaultConfig {
applicationId "com.test"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters 'armeabi-v7a'
}
}
在MainActivity中
// 引入依赖的动态链接库
static {
System.loadLibrary("avutil-55");
System.loadLibrary("avcodec-57");
System.loadLibrary("avformat-57");
System.loadLibrary("avdevice-57");
System.loadLibrary("swresample-2");
System.loadLibrary("swscale-4");
System.loadLibrary("postproc-54");
System.loadLibrary("avfilter-6");
System.loadLibrary("ffmpeg-cmd");
}
// 调用java中的native函数接口
String[] cmd = new String[]{"ffmpeg", "-i", source, "-acodec", "copy", "-vn", target} ;
FFmpegJni.run(cmd) ;
13. 错误记录
-
error: undefined reference to 'postproc_version'
删除cmdutils.c中的
PRINT_LIB_INFO(postproc, POSTPROC, flags, level)
-
error: invalid suffix on literal; C++11 requires a space between literal and identifier [-Wreserved-user-defined-literal] snprintf(name, sizeof(name), "0x%"PRIx64, ch_layout)
修改cmdutils.h中的"0x%"PRIx64
为"0x%" PRIx64
即中间加一个空格
-
出现一些变量名称冲突报错比如B0
修改B0为b0