前言
原来两篇都是在讲FFmpeg的编译,本篇讲在网上最常见的demo:以命令行的形式来使用FFmpeg。
怎么让 FFmpeg 运行命令呢?很简单,调用 FFmpeg 中执行命令的函数即可,这个函数位于源码的 ffmpeg.c 文件中:
int main(int argc, char **argv)
我们的目的很简单:将 FFmpeg 命令传递给 main 函数并执行。而这个传递过程需要编写底层代码实现,在这个底层接口代码中,接收上层传递过来的 FFmpeg 命令 ,然后调用 ffmpeg.c 中的 main 函数执行该命令。
开始集成之前,首先回顾一下 JNI 标准接入步骤:
- 编写带有 native 方法的 Java 类
- 生成该类扩展名为 .h 的头文件
- 创建该头文件的 C/C++ 文件,实现 native 方法
- 将该 C/C++ 文件编译成动态链接库
- 在Java 程序中加载该动态链接库
接下来按照此步骤开始集成,实现 Android 端以命令方式调用 FFmpeg。
准备
Linux 环境(Ubuntu 16.04):64位
NDK版本 :android-ndk-r12b
下载 FFmpeg (ffmpeg-3.2.12): 官网下载链接:https://ffmpeg.org/download.html
1. 编译so库
首先新建一个文件夹 ndkBuild 作为工作空间,在 ndkBuild 目录下新建 jni 文件夹, 作为编译工作目录。
1.1 下载ffmpeg-3.2.12到ndkBuild目录并修改configure文件
前文有
1.2. 编写shell脚本并运行
在ffmpeg-3.2.12目录下
#!/bin/bash
export PLATFORM_VERSION=android-14
function build
{
echo "start build ffmpeg for $ARCH"
./configure --target-os=linux \
--prefix=$PREFIX --arch=$ARCH \
--disable-doc \
--enable-shared \
--disable-static \
--disable-yasm \
--disable-asm \
--disable-symver \
--enable-gpl \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--cross-prefix=$CROSS_COMPILE \
--enable-cross-compile \
--sysroot=$SYSROOT \
--enable-small \
--extra-cflags="-Os -fpic $ADDI_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS" \
$ADDITIONAL_CONFIGURE_FLAG
make clean
make
make install
echo "build ffmpeg for $ARCH finished"
}
ARCH=arm
CPU=arm
PREFIX=$(pwd)/android/$ARCH
TOOLCHAIN=$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
CROSS_COMPILE=$TOOLCHAIN/bin/arm-linux-androideabi-
ADDI_CFLAGS="-marm"
SYSROOT=$NDK_HOME/platforms/$PLATFORM_VERSION/arch-$ARCH/
build
chmod +x build_android.sh
./build_android.sh
在ndkBuild目录下的lib目录下有so库。
1.3 创建Android项目
在Android Studio中创建一个Android项目:AndroidFFmpegSample,新建一FFmpeg.java
package com.example.zjf.androidffmpegsample;
public class FFmpeg {
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");
}
public static native int run(String[] commands);
}
进入AndroidFFmpegSample\app\src\main\java>目录,执行
javah com.example.zjf.androidffmpegsample.FFmpeg
获取C语言的接口函数声明:com_example_zjf_androidffmpegsample_FFmpeg.h。
复制到Ubuntu的工作空间的jni文件夹下,即
ndkBuild/jni
1.4 拷贝文件到jni文件夹
从ffmpeg-3.2.12源码文件拷贝ffmpeg.h ffmpeg.c ffmpeg_opt.c ffmpeg_filter.c cmdutils.c cmdutils.h cmdutils_common_opts.h到jni文件夹下。
1.5 创建 android_log.h 文件
为了将日志输出函数简化为简洁的 “LOGD”、 “LOGE”,需要在同级目录下新建 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
其中 XLOGD 和 XLOGE 方法是为了将 FFmpeg 内部日志信息自动输出到 logcat,后面会用到。
1.6 修改 ffmpeg.c
1.6.1 日志输出到 logcat
在执行命令过程中,FFmpeg 内部的日志系统会输出很多有用的信息,但是在 Android 的 logcat 中是看不到的,所以需要修改源码将 FFmpeg 内部日志输出 logcat 中,方便调试,其实这是十分必要的。修改方法很简单,只需修改 ffmpeg.c 文件三处:
1.6.1.1 引入 android_log.h 头文件
include "android_log.h"
1.6.1.2 修改 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);
}
}
1.6.1.3 设置日志回调方法为 log_callback_null
main 函数开始处
int main(int argc, char **argv)
{
av_log_set_callback(log_callback_null);
int i, ret;
int64_t ti;
init_dynload();
register_exit(ffmpeg_cleanup);
......
1.6.2 执行命令后清除数据
由于 Android 端执行一条 FFmpeg 命令后并不需要结束进程,所以需要初始化相关变量,否则执行下一条命令时就会崩溃。首先找到 ffmpeg.c 的 ffmpeg_cleanup 方法,在该方法的末尾添加以下代码:
nb_filtergraphs = 0;
nb_output_files = 0;
nb_output_streams = 0;
nb_input_files = 0;
nb_input_streams = 0;
然后在 main 函数的最后调用 ffmpeg_cleanup 方法,如下:
......
ffmpeg_cleanup(0);
return main_return_code;
}
1.7 修改cmdutils.c、cmdutils.h
FFmpeg 在执行过程中出现异常或执行结束后会自动销毁进程,而我们在 Android 中调用时,只想让它作为一个普通的方法,不需要销毁进程,只需要正常返回就可以了,这就需要修改 cmdutils.c 中的 exit_program 方法,源码中为:
void exit_program(int ret)
{
if (program_exit)
program_exit(ret);
exit(ret);
}
修改为:
int exit_program(int ret)
{
return ret;
}
此处修改了方法的返回值类型,所以还需要修改对应头文件中的方法声明,即将 cmdutils.h 中的:
void exit_program(int ret) av_noreturn;
修改为:
int exit_program(int ret);
到这里需要修改项都已修改完毕,网上教程实现 FFmpeg 内部日志输出到 logcat 的并不多,但这一步是十分有必要的。很多教程中需要将 ffmpeg 中的 main 方法名字修改为 “run” 、”exec” 等等,其实完全没必要,为什么要对方法名这么在意,乃至不惜徒增新手学习的复杂度呢? 我不知道修改的原因和意义所在。
1.8 拷贝config.h
如果编译的时候报找不到"config.h"的错,就将
ffmpeg-3.2.12目录下的config.h文件拷贝到jni目录下。
1.9 新建com_example_zjf_androidffmpegsample_FFmpeg.c文件
在jni目录下新建com_example_zjf_androidffmpegsample_FFmpeg.c,并在改文件中实现 com_example_zjf_androidffmpegsample_FFmpeg.h 中的方法。
#include "android_log.h"
#include "com_example_zjf_androidffmpegsample_FFmpeg.h"
#include "ffmpeg.h"
JNIEXPORT jint JNICALL Java_com_example_zjf_androidffmpegsample_FFmpeg_run
(JNIEnv *env, jclass obj, jobjectArray commands){
int argc = (*env)->GetArrayLength(env, commands);
char *argv[argc];
int i;
for (i = 0; i < argc; i++) {
jstring js = (jstring) (*env)->GetObjectArrayElement(env, commands, i);
argv[i] = (char*) (*env)->GetStringUTFChars(env, js, 0);
}
LOGD("----------begin---------");
return main(argc, argv);
}
1.10 新建Application.mk
APP_ABI := armeabi-v7a
APP_PLATFORM=android-14
NDK_TOOLCHAIN_VERSION=4.9
1.11 新建Android.mk
LOCAL_PATH:= $(call my-dir)
INCLUDE_PATH:=/home/zjf/workspace/ffmpeg/ndkBuild/ffmpeg-3.2.12/android/arm/include
FFMPEG_LIB_PATH:=/home/zjf/workspace/ffmpeg/ndkBuild/ffmpeg-3.2.12/android/arm/lib
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)
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)
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)
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)
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)
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)
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)
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)
include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := com_example_zjf_androidffmpegsample_FFmpeg.c \
cmdutils.c \
ffmpeg.c \
ffmpeg_opt.c \
ffmpeg_filter.c
LOCAL_C_INCLUDES := /home/zjf/workspace/ffmpeg/ndkBuild/ffmpeg-3.2.12
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice libpostproc
include $(BUILD_SHARED_LIBRARY)
然后执行
ndk-build
ndkBuild/libs目录下就会有armeabi-v7a的so库。
1.12 在Android项目中加载该动态链接库
将 libs 目录下生成的 armeabi-v7a 动态库拷贝到 Android 工程中libs目录下。
在 FFmpeg.java 中加载动态库:
public class FFmpeg {
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");
}
public static native int run(String[] commands);
}
记得在应用的 build.gradle 文件中 android 节点下添加动态库加载路径:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
1.13 实现Android程序
Demo中的功能是:将input.mp4 和 input.mp3 两个文件合并成一段以input.mp3为背景音乐的 merge.mp4 文件(源码中上传了input.mp4 和 input.mp3 两个文件)。
activity_main添加一个button控件
<Button
android:onClick="run"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="运行CMD"/>
Demo需要SD卡读写权限,所以先加两个权限:SD卡读写权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
直接给出MainActivity代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if ( ActivityCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ) {
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
}
}
public void run(View view) {
String base = Environment.getExternalStorageDirectory().getPath();
Log.e("PATH", base);
String[] commands = new String[9];
commands[0] = "ffmpeg";
commands[1] = "-i";
commands[2] = base + "/input.mp4";
commands[3] = "-i";
commands[4] = base + "/input.mp3";
commands[5] = "-strict";
commands[6] = "-2";
commands[7] = "-y";
commands[8] = base + "/merge.mp4";
int result = FFmpeg.run(commands);
if(result == 0){
Toast.makeText(MainActivity.this, "命令行执行完成", Toast.LENGTH_SHORT).show();
Log.e("RESULT = ", Integer.toString(result));
}
}
}