转载自:https://blog.csdn.net/yhaolpz/article/details/76408829?locationNum=2&fps=1,但是根据这篇教程,有各种问题,所以自己记录一下自己的学习经历。
前言
网上关于 Android 集成 FFmpeg 的文章很多,但大多数都只介绍了步骤,没有说明背后的原理,若之前没有集成底层库的经验,那就会“神知无知”的走一步看一步,出错几率很大,出错了也不知道原因,然后会乱猜“这篇教程有问题“,“换个版本估计可以”,甚至“电脑有问题,重装下系统试试”。
为什么会出现这种情况,答案很简单:欲速则不达,要实现 Android 端集成 FFmpeg 功能,那就要掌握必需的基础知识,如果连 JNI、NDK都不了解,一上来就参考几秒钟搜出来的集成步骤开始集成,那只会被各种莫名其妙的异常完虐,如果运气好很快就实现了功能呢? 在我看来这是更大的损失,这些你没有掌握的知识如此接近却又悄悄溜走。
那么在 Android 端集成 FFmpeg 需要掌握哪些基础知识呢?个人认为以下内容是需要了解的:
- JNI
- CPU架构
- 交叉编译
- NDK
- FFmpeg 简介
1. JNI
JNI,即 Java Native Interface ,是 Java 提供用来与其他语言通信的 api ,“其他语言”意味不止局限于 C 或 C++ ,也可以调用除 C 和 C++ 之外的语言,只是大多数情况下调用 C 或 C++ ; “通信”意味着 Java 和 其他语言之间可以相互调用,不止局限于 Java 调用其他语言,其他语言也可以主动调用 Java .
Java 虚拟机实现了跨平台特性, 无法很好的实现与操作系统相关的本地操作,而 C 或 C++ 可以,同时代表着 C 或 C++ 不具备 Java 的跨平台能力,那么当我们在程序中使用 JNI 功能时,就必须关注程序的平台可移植性, JNI 标准要求本地代码至少能工作在任何Java 虚拟机环境。
简而言之,跨平台的 Java 调用了不跨平台的 C/C++,使程序丧失了跨平台性,这就是 JNI 的副作用,所以可以不使用 JNI 时就尽量避免。而大多数不可避免的情况是:已存在用 C/C++ 写的程序/库或者 Java 语言不支持程序所要实现的特性,比如 ffmpeg 是由 C 编写的,则必须要通过 JNI 实现调用。
JNI 的实现步骤很简单,如下:
- 编写带有 native 方法的 Java 类
- 生成该类扩展名为 .h 的头文件
- 创建该头文件的 C/C++ 文件,实现 native 方法
- 将该 C/C++ 文件编译成动态链接库
- 在Java 程序中加载该动态链接库
动态链接库是一组源代码的模块,其中包含可供应用程序调用的函数。比如 Windows 下的 .dll 文件就是一种动态链接库,也就是说 Java 程序在 Windows 中运行,所需的动态链接库就是 .dll 文件; 如果 Java 程序在 Linux 中运行,所需的动态链接库就是 .so 文件 ,这里 JNI 的副作用已初见端倪,本来无视操作系统的 Java ,因为 JNI ,就要考虑运行环境是 Windows 还是 Linux 。除此之外,还要考虑 CPU 架构,这也是 Android 中使用 JNI 主要需考虑的 so 库兼容型问题。
Android是Linux发行版本,所以所需要的动态链接库就是 .so 文件。
对于 Java 程序来说,需要的仅仅是编译后的动态链接库,不需要 C/C++ 文件和 .h 头文件。Android 亦如此,网上很多 Android NDK 教程会把需要编译的 C/C++ 源码放入 Android 工程中,这对新手来说可能会产生误导,误以为 Android 工程需要 C/C++ 文件或 .h 头文件或者其他的文件,要清楚的是, Android 工程需要的仅仅是编译后的 .so 库,所以我们可以在工程之外编译完后,只将 .so 库移植到工程中。那为什么大多数教程会把源码先移植到 Android 工程中再去编译呢?目的只有一个:节省目录的切换以及 .so 库的复制时间,实际上这些时间微乎其微,我推荐新手将编译操作于工程外进行,更易理解。
2. CPU架构
我们都知道 CPU 是什么,那 CPU 架构到底是什么呢?回归到“架构”这个词本身含义,CPU 架构就是 CPU 的框架结构、设计方案,处理器厂商以某种架构为基础,生产自己的 CPU,就好比“总-分-总”是文章的一种架构,多篇文章可以都基于“总-分-总”架构。
常见的 CPU 架构有 x86、x86-64 以及 arm 等, x86-64 其实也是基于 x86 架构,只是在 x86 的基础上做了一些扩展,以支持 64 位程序的应用,常见的 Intel 、AMD 处理器都是基于 x86 架构的。
而 x86 架构主打的是 pc 端,对于移动端,arm 架构处于霸主地位 ,由于其体积小、低功耗、低成本、高性能的优点,被广泛应用在嵌入式系统中,目前大多数安卓、苹果手机的 CPU 都基于 arm 架构,此处所说的 arm 架构指 arm 系列架构,其中包括 ARMv5 、ARMv7 等等。
最后再看 Android 端 , Android 系统目前支持 ARMv5、ARMv7、ARMv8、 x86 、x86_64、MIPS 以及 MIPS64 共七种 CPU 架构,也就是说除此之外其他 CPU 架构的硬件并不能运行 Android 系统。
3. 交叉编译
在某个平台上,编译该平台的可执行程序,叫做本地编译,比如在 Windows 平台上编译 Windows 自身的可执行程序;在 x86 平台上,编译 x86 平台自身的可执行程序。
在某个平台上,编译另一种平台的可执行程序,就是交叉编译,比如在 x86 平台上,编译 arm 平台的可执行程序,这也是 Android 端使用最多的交叉编译类型。
在交叉编译时,由于主机与目标的体系架构、环境不同,所以交叉编译比本地编译复杂很多,需要一些工具来解决主机与目标不同特性的问题,这些工具构成的工具集就叫做交叉编译链。
既然交叉编译比本地复杂很多,那为什么不使用本地编译,比如在 arm 平台编译 arm 平台的可执行程序呢?这是因为目标平台存储空间和计算能力通常是有限的,而编译过程需要较大的存储空间和较快的计算能力,但目标平台无法提供。
4. NDK
我们需要的是 arm 平台的动态库,而这一编译过程往往是在 x86 平台上进行,所以属于交叉编译,需要交叉编译链来实现,所以 NDK(Native Development Kit )中提供了交叉编译链,方便开发。
Android 中包括七种 CPU 架构,NDK 中自然就有与之对应的交叉编译链,以下是 Android 官网对此的表格描述:
编译工具链
除此之外,NDK 还提供了一些原生标头和共享库文件,包括 C/C++ 支持库、从 C/C++ 代码中可以向 Android 系统输出日志的 < android/log.h > 等等,总之,NDK 是用来帮助我们实现交叉编译的工具。
在实际使用时,比较重要的是 Android.mk 语法,内容并不多,但你必须了解,不然只复制别人的配置很容易出错,关键是你无法真正的掌握这部分知识,而最好的学习方法就是仔细阅读几遍 Android.mk 官网教程 。
另外还需要了解什么是 ABI ,ABI 即 application binary interface ,应用程序二进制接口,顾名思义,“二进制接口”说明这是程序与系统之间的底层接口,它定义了程序如何与系统交互。我们应该指定每个 CPU 架构所对应的 ABI,所以 Android 中就出现了 armeabi 、armeabi-v7a、arm64-v8a、x86、x86_64、mips 以及 mips64 目录来区分不同的 ABI ,我们将编译好的动态库放入对应 CPU 架构的 ABI 目录中就可以了。
掌握了以上知识点,才能知道 Android 集成 FFmpeg本质上是在做什么,为什么要这样做。不只是集成 FFmpeg,这些知识对于任何底层库的集成都是通用、必要的。
5. FFmpeg 简介
FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序
下面分为 3 个部分介绍 FFmpeg的编译及 Android 端的简单调用:
- 准备
- 编译 FFmpeg
- 编译动态库及调用
5.0 前提条件
- 在Ubuntu 16.04中要先进行NDK的安装及配置,还有编译工具链的配置
- Windows系统下配置好Java环境变量
5.1 准备
Linux 环境(Ubuntu 16.04):64位
下载NDK(android-ndk-r12b) :原博客中用的是android-ndk-r14b
下载 FFmpeg (ffmpeg-3.2.12): 官网下载链接:https://ffmpeg.org/download.html
5.2. 编译 FFmpeg
编译环境为 x86_64 的 Ubuntu16.04,运行环境为 arm 架构的 Android 系统,目标是把 FFmpeg 源码编译成 Android 端可调用的动态库,这属于交叉编译,所以需要 NDK 提供的交叉编译工具,这是这一步骤的本质意义。
Android 工程中只支持导入 .so 结尾的动态库,形如:libavcodec-57.so 。但是FFmpeg 编译生成的动态库默认格式为 xx.so.版本号 ,形如:libavcodec.so.57 , 所以需要修改 FFmpeg 根目录下的 configure 文件,使其生成以 .so 结尾格式的动态库:
# 将 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)'
在编译 FFmpeg 之前,我们需要修改 FFmpeg 的编译选项,主要目的如下:
- 规定编译方式,使其通过交叉编译生成我们需要的动态库。
- 选择所需功能,针对需求定制 FFmpeg 功能,精简动态库。
怎么修改 FFmpeg 的编译选项呢?在 FFmpeg 根目录下通过 ./configure 命令进行设置,但是为了方便记录与修改,我们选择在根目录下建立一个脚本文件来运行 ./configure 命令。与原博客不同,用了他的sh文件后面会报错。
#!/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
除强迫症风格的注释之外,再对个别配置进行说明:
-
**–sysroot=$SYSROOT **: 前言中提到 NDK 除了提供 交叉编译链 以外,还提供一些原生标头和共享库文件,通过此配置指定了交叉编译环境,使其在编译过程中能够引用到 NDK 提供的原生标头和共享库文件,其中 android-14 目录指定了生成动态库最低支持的 Android 版本,嗯~ ,这里可以说它是向后兼容的。
-
–target-os=linux :Android 内核为 Linux ,故在此指定为 linux ,如果要编译的目标系统为 ios ,则指定为 darwin 。
-
–cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- : 类似于通配符方式指定 bin 目录下以 arm-linux-androideabi-
接下来运行该脚本文件使配置生效,比如我的脚本文件名为 build_android.sh :
./build_android.sh
然后就可以看到成功生成:
lib目录下是编译出来的FFmpeg的so库:
lib目录文件
include目录下是头文件:
include目录文件
5.3 编译动态库及调用
你可能会疑问,上一步已经编译 FFmpeg 源码生成动态库了,为什么这一步还是“编译动态库”呢?其实这个问题等效于:上一步中生成的动态库可以直接在 Android 工程中使用吗?
答案是否定的,回到文章开头 JNI 的使用步骤,在编写带有 native 方法的 Java 类后,紧接着就是用 C/C++ 实现本地接口,这是 Java 与 C/C++ 交互的必要通道。
所以接下来需要编写本地接口,在本地接口中调用上一步编译好的 FFmpeg 动态库,然后将本地接口也编译成动态库,供 Android 调用,这一步还需要“编译动态库”。
网上有些教程在这一步把 FFmpeg 源码、动态库全部复制到 Android 工程中,然后在工程中新建本地接口、mk文件…… 花里胡哨的看的我头皮发麻~ 这一步我推荐在 Android 工程之外进行,直到生成最终可用的动态库之后,再拷贝到 Android 工程中直接使用。
首先新建一个文件夹,取名随意,比如 “ndkBuild ”,这个目录就作为我们的工作空间,然后在 ndkBuild 下新建 jni 文件夹, 作为编译工作目录。 OK~ 接下来按照前言中的 jni 步骤来划分操作:
5.3.1 在Android Studio中新建一个项目,编写带有 native 方法的 Java 类:
package com.example.zjf.ffmpegjnidemo;
public class FFmpeg {
public static native void run();
}
5.3.2 生成该类扩展名为 .h 的头文件
在Android Studio的Terminal中的项目目录下执行:
javah com.example.zjf.ffmpegjnidemo.FFmpeg
网上很多教程需要先生成 .class 文件,而我在 java 1.8 环境下亲测以上一句命令即可。
如果需要生成.class文件时,要在Android Studio的Terminal中的项目目录下执行:
javac com/example/zjf/ffmpegjnidemo/FFmpeg.java
网上很多教程说是这种格式:
javac com.example.zjf.ffmpegjnidemo.FFmpeg.java
其实是错误的,要是"/"。
5.3.3 创建该头文件的 C/C++ 文件,实现 native 方法
将生成的 com_example_zjf_ffmpegjnidemo_FFmpeg.h 文件剪切到 ndkBuild 的 jni 目录下,创建对应的 C 文件 com_example_zjf_ffmpegjnidemo_FFmpeg.c :
#include <android/log.h>
#include "com_example_zjf_ffmpegjnidemo_FFmpeg.h"
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/avfilter.h"
JNIEXPORT void JNICALL Java_com_example_zjf_ffmpegjnidemo_FFmpeg_run(JNIEnv *env, jclass obj) {
char info[40000] = {0};
av_register_all();
AVCodec *c_temp = av_codec_next(NULL);
while(c_temp != NULL){
if(c_temp->decode!=NULL){
sprintf(info,"%s[Dec]",info);
}else{
sprintf(info,"%s[Enc]",info);
}
switch(c_temp->type){
case AVMEDIA_TYPE_VIDEO:
sprintf(info,"%s[Video]",info);
break;
case AVMEDIA_TYPE_AUDIO:
sprintf(info,"%s[Audio]",info);
break;
default:
sprintf(info,"%s[Other]",info);
break;
}
sprintf(info,"%s[%10s]\n",info,c_temp->name);
c_temp=c_temp->next;
}
__android_log_print(ANDROID_LOG_INFO,"myTag","info:\n%s",info);
}
这段程序用于输出 FFmpeg 支持的编解码信息,通过 < android/log.h > 的 __android_log_print 方法可以直接将信息输出到 Android Studio 的 logcat 。
5.3.4 将该 C/C++ 文件编译成动态链接库
编译 FFmpeg 源码时的实际入口是通过 FFmpeg 提供的 makefile ,而在这一步,将直接使用 NDK 提供的编译方法,需要提供 Application.mk 和 Android.mk 文件。
首先在 jni 目录下创建 Application.mk 文件 :
APP_ABI := armeabi
APP_PLATFORM=android-14
APP_ABI 表示编译生成 armeabi 架构的 so 库,APP_PLATFORM 表示最低支持的 Android 版本。
然后在 jni 目录下创建 Android.mk 文件:
LOCAL_PATH:= $(call my-dir)
INCLUDE_PATH:=/home/zjf/workspace/ffmpeg-3.2.12/android/arm/include
FFMPEG_LIB_PATH:=/home/zjf/workspace/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_LIBRAR)
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_ffmpegjnidemo_FFmpeg.c
LOCAL_C_INCLUDES := /home/zjf/workspace/ffmpeg-3.2.12
LOCAL_LDLIBS := -lm -llog
LOCAL_SHARED_LIBRARIES := libavcodec libavfilter libavformat libavutil libswresample libswscale libavdevice libpostproc
include $(BUILD_SHARED_LIBRARY)
可能报的错:
- ffmpeg 源码编译找不到 libpostproc 库
在build_android.sh文件中给 ./configure 加参数
--enable-postproc
--enable-gpl- Android NDK: Module ffmpeg depends on undefined modules: swresample
用android-ndk-r14b编译的时候报错,换了低版本(android-ndk-r12b)之后就可以了
对于 Android.mk 文件前面提到过,最好的学习方法就是仔细阅读几遍 Android.mk 官网教程 。 简要说明一下,此文件将之前编译好的 FFmepg 动态库通过 NDK 提供的预编译方式编译,通过 prebuilt 这个单词也能猜到它的含义,点击了解 NDK 预编译, 最后将 com_jni_FFmpeg.c 编译成名为 ffmpeg 的动态库。
此时 jni 目录下应有以下四个文件:
jni目录文件
然后在 jni 目录下运行 (注意要把 ndk 添加到环境变量):
ndk-build
大功告成 ,此时在 ndkBuild 目录下生成了 libs 和 obj 目录,而 Android 需要的最终的动态库就在 libs 目录下,最终会在 libs 目录下生成libffmpeg.so 。
5.3.5 在Java 程序中加载该动态链接库
将 libs 目录下的 armeabi 文件夹整体拷贝到 Android Studio 工程的 libs 文件夹下,当然如果你的工程已经存在 armeabi 目录,就把该目录下的动态库拷贝到工程的 armeabi 目录下。
我生成的libs库下少一个swresample-,可以把ffmpeg-3.2.12/android/arm/lib目录下的so库拷贝过去。
在 FFmpeg 中加载动态库:
package com.example.zjf.ffmpegjnidemo;
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 void run();
}
记得在应用的 build.gradle 文件中 android 节点下添加动态库加载路径:
sourceSets.main {
jniLibs.srcDir 'libs'
jni.srcDirs = []
}
然后在程序中调用 FFmpeg.run() 方法 ,可以看到 logcat 输出了 FFmpeg 的编解码信息:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FFmpeg.run();
}
}
运行结果:
运行结果
源码地址:https://github.com/Xiaoben336/FFmpegJNIDemo
后记
前面经常提到NDK,需要安装和配置NDK,下载NDK,解压到任意目录,最好不要是root权限目录,然后在/etc/profile文件中配置NDK的环境变量,注意的是,交叉编译链也要配置,很多教程都忘了。一般source /etc/profile重启后才会生效,也可以用echo $NDK_HOME查看
#set NDK env
export NDK_HOME=/home/zjf/Android/Ndk/android-ndk-r12b
export ARM_TOOL=/home/zjf/Android/Ndk/android-ndk-r12b/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin
export PATH=$NDK_HOME:$ARM_TOOL:$PATH
#set JDK env
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_191
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
#set SDK env
export ANDROID_SDK_HOME=/home/zjf/Android/Sdk/android-sdk-linux
export PATH=$PATH:${ANDROID_SDK_HOME}/tools
export PATH=$PATH:${ANDROID_SDK_HOME}/platform-tools
值得注意的是,平台需要21以上才有64位交叉编译链。感谢@大牙的提醒,开始我用android-19,找不到arch-arm64子目录
网友评论