本系列博客专述如何使用 FFmpeg 实现一个简单的播放器,总共分为 6 篇,主要包括了Android FFmpeg 环境搭建、音频解码与播放、视频解码与播放、音视频同步、快进/快退、暂停与停止。其中涉及到了 C++ 基础,多线程、OpenSL ES、 Surface、FFmpeg 解码等技术。
新建项目
选择一个空白 Activity, 命名自定义,如下图所示
配置 CMake 环境
在 src/main 目录下新建一个 cpp 目录, 用来存放 C/C++ 源代码。
配置 CMakeLists.txt, 这个文件是 CMake 编译 C/C++ 源码时必须创建的。关于 CMake 点击此处阅读。简单入门请参考 CMake 零基础入门。有了源文件,虽然里面还没有内容,暂时不需要关注。需要告诉编译系统怎样编译源文件,打包到 apk 中使用,要想 Android 系统能调用 C/C++ 写的库,必须将源码编译后打包成动态库 .so 文件。 有如下几个步骤:
- 在 cpp 目录下新建 CMakeLists.txt 文件,配置内容如下
# 支持 cmake 最小版本为 2.6
cmake_minimum_required(VERSION 2.6)
# 查找所有源文件, 将其存放到 SRC_DIR 变量中.
aux_source_directory(. SRC_DIR)
# 编译源文件生成链接库
add_library(native-lib SHARED ${SRC_DIR})
# 链接系统日志库 log 到 native-lib
target_link_libraries(native-lib log)
- 配置 app 目录下的 build.gradle 文件,配置如下
android {
......
externalNativeBuild {
cmake {
path 'src/main/cpp/CMakeLists.txt'
}
}
}
- 在 MainActivity 中添加如下方法,测试 java 调用 JNI 方法
// 这里的静态代码库很重要,如果忘了写就会报错
static {
System.loadLibrary("native-lib");
}
// JNI 方法,获取一个 JNI 方法的消息.
public native String getMessageFromJNI();
- 创建 JNI 方法,这里有两种生成方法: (1)通过 javah 方法生成,但是在 jdk10 以后 javah 被移除,通过 javac 替代。(2)掌握 JNI 生成规则后手写。这里采用手写方式,规则如下
// 这个是普通方法,非静态
extern "C"
JNIEXPORT 返回值 JNICALL
Java_包全路径_类名_方法名(JNIEnv *, jobject)
// 静态方法,参数有区别,因为静态方法是优先于类被创建的
extern "C"
JNIEXPORT 返回值 JNICALL
Java_包全路径_类名_方法名(JNIEnv *, jcalss)
知道了规则后,就可以创建本地方法。FFmpegPlayer.cpp 中有如下方法
#include <jni.h>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_hxj_ffmpegplayer_MainActivity_getMessageFromJNI(JNIEnv *env, jobject instance) {
return env->NewStringUTF("Hello Java. I'm from JNI.");
}
现在就可以运行测试了,最好在真机上测试。
FFmpeg 源码集成
通过上面的配置,你已经拥有了一个可以支持 NDK 开发的环境啦!接下来就是要引入重头戏 FFmpeg 啦!希望看到这里时,你已经对 FFmpeg 有了一定了解,并能在自己的本地环境中交叉编译出静态库/动态库啦!如果你是一个小白,请先阅读 FFmpeg 模块介绍 和 Mac 平台 FFmpeg4.2.1 交叉编译。现在开始讲解如何引入 FFmpeg, 分为如下几个步骤:
- 在 cpp 目录下,创建 ffmpeg 目录,在 ffmepg 下创建 include 目录和 libs 目录
解释这两个目录的作用,include 目录用来存放 FFmpeg 头文件,libs 目录用来存放静态库文件,如果你编译的是动态库,那么需要存放到 app/src/main/jniLibs
目录显示如下
- 修改 CMakeLists.txt 文件,指定 FFmpeg 头文件,将 ffmpeg 静态库链接到 native-lib 中, 配置文件每一行都有说明。
# 支持 cmake 最小版本为 2.6
cmake_minimum_required(VERSION 2.6)
# 查找所有源文件, 将其存放到 SRC_DIR 变量中.
aux_source_directory(. SRC_DIR)
# 将 ffmpeg 目录下的 include 文件加入到系统查找路径
include_directories(${CMAKE_SOURCE_DIR}/ffmpeg/include)
# 通过指定 CMAKE_CXX_FLAGS 参数告诉编译器 ffmpeg 静态库的查找路径. -L:表示静态库/动态库查找路径
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/ffmpeg/libs/${CMAKE_ANDROID_ARCH_ABI}/")
# 源文件编译生成链接库
add_library(native-lib SHARED ${SRC_DIR})
# 链接系统日志库 log 以及 ffmpeg 静态库到 native-lib
# 请注意这里的 -Wl,--start-group -Wl,--end-group, 因为 FFmepg
# 各静态库之间相互依赖,为了防止顺序依赖错误,加入这两个配置可以自动去解决依赖关系
target_link_libraries(
native-lib
log
-Wl,--start-group
avcodec avfilter avformat avutil swresample swscale
-Wl,--end-group
)
- 光配置 CMakeLists.txt 编译会报错,因为我们还没有指定编译哪些平台。细心的同学可能发现在 libs 目录下还有个 armeabi-v7a 目录,这个就是针对 armeabi cpu 架构生成的动态库。但是目前以及逐步向 64 的架构推进了,具体信息参考 g.co/64-bit-requ…。
android {
compileSdkVersion 28
buildToolsVersion "29.0.2"
defaultConfig {
......
// 指定生成 armeabi-v7a 的动态库. 支持的 API 有[arm64-v8a, armeabi-v7a, x86, x86_64]
externalNativeBuild {
cmake {
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
}
......
}
这个时候可以重新编译,看看是否成功。
- 修改 FFmpegPlayer.cpp, 请注意导包需要加入 extern "C", 对这个不了解的 点击此处阅读
#include <jni.h>
extern "C" {
#include "libavcodec/avcodec.h"
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_hxj_ffmpegplayer_MainActivity_getMessageFromJNI(JNIEnv *env, jobject instance) {
// 打印 FFmepg 版本信息
return env->NewStringUTF(av_version_info());
}
编译运行,看到显示 4.2.1 版本即成功,值得注意的是,需要使用真机运行。需要在模拟器中运行,需要编译 FFmpeg x86 架构的动态库。