本文将会介绍:如何在 Docker 下为 Android 编译 FFMpeg 动态库。
1 前言
为什么使用 Docker
Docker 相当于一个虚拟机,类似于 Vmware Workstation。使用 Docker 可以充分保证(容器内)环境的一致性,减少不同环境的干扰。
基础概念
- 镜像(image):有过装系统经验的应该不难理解,
- 宿主机(host):运行 Docker engine 的环境,可以理解为你的电脑正在运行的系统(当然还包括硬件)。
- 容器(container):通过镜像创建的实体,一个镜像可以创建多个容器。
- 交叉编译(cross compile):通俗点说,是在一个架构的环境下,编译另一个架构下可以运行的目标文件(动态库、静态库、可执行文件等)。
2 环境
为确保之后的编译步骤顺畅进行,在此将我所使用的环境列出来:
- 镜像:ubuntu:18.04
- 宿主机:macOS Catalina 10.15.7
- FFMpeg 源码版本:5.0
- NDK 版本:23.1.7779620
理论上你应将除宿主机以外的环境跟笔者保持一致。
3 步骤
3.1 宿主机操作
创建容器
docker run -it -d ubuntu:18.04 /bin/bash
这条命令将会自动下载 ubuntu:18.04
镜像(如果本地没有),然后创建并进入该容器。
后续所有步骤/命令,均在容器内进行/执行。
3.2 容器内操作
3.2.1 更新软件源
cd ~ && apt update
进入容器的默认用户身份是
root
(默认当前路径是/
),因此执行命令不需要sudo
。
3.2.2 安装必要软件包
apt install build-essential curl zip openjdk-8-jdk vim -y
介绍下各软件包的作用:
-
build-essential
:Ubuntu 上基础编译软件的工具大集合。 -
curl
:这里被用来下载文件。 -
zip
:解压 zip 文件用的。 -
openjdk-8-jdk
:部分 command line tools 需要 JAVA 环境才能执行。 -
vim
:文本编辑。
3.2.3 准备 ffmpeg 源码
# 下载
curl -OL https://www.ffmpeg.org/releases/ffmpeg-5.0.tar.xz
# 解压
tar xvJf ffmpeg-5.0.tar.xz
3.2.4 准备 NDK
# 下载 Command Line Tools
curl -OL https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip
# 解压 Commmand Line Tools
unzip commandlinetools-linux-7583922_latest.zip
# 设置 android sdk 目录
mkdir -pv ~/.local/android
# 配置 Command Line Tools
mkdir -pv ~/.local/android/cmdline-tools/
mv ~/cmdline-tools ~/.local/android/cmdline-tools/latest
# 添加环境变量
echo "export PATH=$HOME/.local/android/cmdline-tools/latest/bin:$PATH" >> ~/.bashrc
# 使环境变量生效
source ~/.bashrc
# 安装 NDK,注意这里需要同意下协议!!
sdkmanager --install "ndk;23.1.7779620"
安装好的 NDK 将会在 $HOME/.local/android/ndk/23.1.7779620/
路径。
3.2.5 编译配置选项
ffmpeg 功能十分丰富,因而有相当多的配置选项,主要用于配置功能的开关,可以通过 ./configure --help
查看。
如果是线上环境使用,为了商业合规、控制包体积,我们需要根据开源协议、实际所需功能,进行裁剪。
此处主要目的是学习,因此将常用、尽可能多的功能打开。
ffmpeg 主要有以下几大模块:
-
libavcodec
:音视频的编解码库。 -
libavdevice
:与多媒体设备交互的库。 -
libavfilter
:滤波器库。音频的算法处理、视频的滤镜等等。 -
libavformat
:多媒体文件的格式和协议的封装、解封库。如 mp4 文件格式,rtmp 网络协议。 -
libavutil
:ffmpeg 里面的工具类 -
libpostproc
:后处理库。 -
libswresample
:重采样库。 -
libswscale
:图像缩放、颜色空间和图像格式转换库。
# 切换到 ffmpeg 源码目录
cd ~/ffmpeg-5.0
# 创建编译脚本
vim compile_ffmpeg.sh
注意:自行了解 vim 使用。
以下是配置脚本内容。
#!/bin/bash
# filename: compile_ffmpeg.sh
set -e
API=29
OS=android
PREFIX="${pwd}/out/"
ARCH=arm64
CPU=armv8-a
CFLAGS="-Os"
ANDROID_HOME=$HOME/.local/android
NDK=$ANDROID_HOME/ndk/23.1.7779620/
TOOLCHAINS=$NDK/toolchains/llvm/prebuilt/linux-x86_64
CC=$TOOLCHAINS/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAINS/bin/aarch64-linux-android$API-clang++
SYSROOT=$TOOLCHAINS/sysroot
CROSS_PREFIX=$TOOLCHAINS/bin/aarch64-linux-android-
NM=$TOOLCHAINS/bin/llvm-nm
STRIP=$TOOLCHAINS/bin/llvm-strip
PKG_CFG=$TOOLCHAINS/bin/llvm-config
function build_ffmpeg
{
echo "Start build ffmpeg...for $CPU"
SECONDS=0
./configure \
--prefix=$PREFIX \
--disable-static \
--enable-shared \
--arch=$ARCH \
--cpu=$CPU \
--target-os=$OS \
--cc=$CC \
--cxx=$CXX \
--enable-cross-compile \
--cross-prefix=$CROSS_PREFIX \
--sysroot=$SYSROOT \
--nm=$NM \
--strip=$STRIP \
--pkg-config=$PKG_CFG \
--enable-jni \
--enable-mediacodec \
--enable-pic \
--enable-hwaccels \
--disable-doc \
--extra-cflags=$CFLAGS \
--extra-cxxflags=$CXXFLAGS \
make -j
make install
duration=$SECONDS
echo "Compile for $CPU success! cost time $(($duration / 60)) mins $(($duration % 60)) seconds"
}
# 编译 Arm 64 位架构
ARCH=arm64
CPU=armv8-a
PREFIX=$(pwd)/out/$OS/$CPU
build_ffmpeg
# 编译 Arm 32 位架构
#ARCH=arm
#CPU=armv7-a
#PREFIX=$(pwd)/out/$OS/$CPU
#build_ffmpeg
编译脚本将会一直维护更新:build ffmpeg 5.0 with latest NDK on ubuntu 18.04 using Docker
3.2.6 开始编译
# 给编译脚本加上执行权限
chmod u+x compile_ffmpeg.sh
# 开始编译
./compile_ffmpeg.sh
3.2.7 编译产物
编译产物将会在 out/android/${CPU}
目录下。
# 以这里为例,64 位的编译产物将会在下面这个路径
ls -hl ~/ffmpeg-5.0/out/android/armv8-a/
4 集成
下面将会讲通过 Android Studio 集成 ffmpeg 动态库到 Android 项目中。
4.1 配置环境
4.1.1 创建存放动态库的文件夹
# 创建动态库的文件夹,这里命名为 libs
mkdir -pv $PROJECT_ROOT/app/src/main/libs
# 创建存放特定架构动态库的文件夹,这里创建了存放 arm 64 位的动态库文件夹
mkdir -pv $PROJECT_ROOT/app/src/main/libs/arm64-v8a
注意:
-
libs
文件夹的名字可以任取,只要不是jniLibs
,否则需要做些特殊配置。 - 创建存放特定架构动态库的文件夹名字建议与
ANDROID_ABI
保持一致,方便后续在 CMakeLists.txt 中使用。
4.1.2 复制动态库到项目
# 先想办法把步骤 3.2.7 的编译产物 arm 64 位动态库从容器中弄出来
# 然后复制到上面 4.1.1 创建的文件夹 $PROJECT_ROOT/app/src/main/libs/arm64-v8a 下
这一步骤后,项目工程大概长这样:
4.1.3 创建 native 编译文件
# 创建 cpp 文件夹,用于存放 c/c++ 源码
mkdir -pv $PROJECT_ROOT/app/src/main/cpp
# 新建 FFMpegJNI.cpp 文件,用于实现这里的 JNI 接口
# 新建 CMakeLists.txt 文件
touch $PROJECT_ROOT/app/src/main/cpp/CMakeLists.txt
CMakeLists.txt 文件的内容为:
cmake_minimum_required(VERSION 3.18.1)
project("ffmpegturtorial")
set(CMAKE_CXX_STANDARD 17)
# 创建变量,声明了 ffmpeg 动态库的位置,将会根据 abi 有不同的区分
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../libs/${ANDROID_ABI})
# 创建变量,声明了 ffmpeg 头文件的位置
set(ffmpeg_include_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
# 添加预构建的 ffmpeg 动态库到项目中
# ref:https://developer.android.com/studio/projects/configure-cmake#add-other-library
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so)
add_library(avdevice SHARED IMPORTED)
set_target_properties(avdevice
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavdevice.so)
add_library(avfilter SHARED IMPORTED)
set_target_properties(avfilter
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavfilter.so)
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavformat.so)
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswresample.so)
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswscale.so)
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavutil.so)
add_library(ffmpeg
SHARED
FFMpegJNI.cpp)
find_library(log-lib
log)
target_include_directories(ffmpeg
PRIVATE
${ffmpeg_include_dir}
)
target_link_libraries(
ffmpeg
# 链接到 ffmpeg 动态库
avcodec
avdevice
avfilter
avformat
swresample
swscale
avutil
${log-lib})
4.1.4 复制 ffmpeg 头文件到项目
头文件在容器内的 ~/ffmpeg-5.0/out/android/armv8-a/include
目录下。
复制完以后,项目工程大概长这样:
4.1.3 build.gradle
文件配置
主要修改模块的 build.gradle
文件。
android {
// ...
defaultConfig {
// ...
// 配置 native 代码的一些默认参数
externalNativeBuild {
cmake {
cppFlags ''
}
}
ndk {
// !!!!!重点注意这里!!!!!
// 只构建 arm 64 位的 native 库,因为上面只提供了 arm 64 位的 ffmpeg 库
abiFilter "arm64-v8a"
}
}
// native 库的构建方式
externalNativeBuild {
// 采用 cmake 构建
cmake {
// CMakeLists.txt 文件的位置
path file('src/main/cpp/CMakeLists.txt')
// 指定 cmake 的版本,要求不小于 CMakeLists.txt 声明的
version '3.18.1'
}
}
}
4.2 Demo 源码
4.2.1 MainActivity.kt
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
// jni 库的名字跟 CMakeLists.txt 中的保持一致
private const val FFMPEG_LIBRARY = "ffmpeg"
init {
// 注意在 init 语句块中加载 jni 库
try {
System.loadLibrary(FFMPEG_LIBRARY)
Log.i(TAG, "load ffmpeg library success")
} catch (e: Exception) {
Log.d(TAG, e.message, e)
}
}
}
// 定义一个 native 方法
// 这个函数的作用是返回 ffmpeg 的一些版本、构建信息
private external fun getFFMpegVersion(): String
}
4.2.2 FFMpegJNI.cpp
#include <jni.h>
#include <string>
// 尤其注意这里,ffmpeg 是基于 c 构建的,在 include 它的头文件时,也必须以 c 的方式引入,
// 否则链接时会出现符号异常,提示找不到符号。
extern "C" {
#include "libavcodec/version.h"
#include "libavcodec/avcodec.h"
#include "libavfilter/version.h"
#include "libavformat/version.h"
#include "libswscale/version.h"
#include "libswresample/version.h"
}
extern "C"
JNIEXPORT jstring JNICALL
Java_me_hjhl_app_ffmpegturtorial_MainActivity_getFFMpegVersion(JNIEnv *env, jobject thiz) {
std::string def;
def.append("libavcodec: " AV_STRINGIFY(LIBAVCODEC_VERSION) "\n");
def.append("libavfilter: " AV_STRINGIFY(LIBAVFILTER_VERSION) "\n");
def.append("libavformat: " AV_STRINGIFY(LIBAVFORMAT_VERSION) "\n");
def.append("libavutil: " AV_STRINGIFY(LIBAVUTIL_VERSION) "\n");
def.append("libswscale: " AV_STRINGIFY(LIBSWSCALE_VERSION) "\n");
def.append("libswresample: " AV_STRINGIFY(LIBSWRESAMPLE_VERSION) "\n");
def.append("avcodec license: ");
def.append(avcodec_license());
def.append("\n");
def.append("build command: ./configure ");
def.append(avcodec_configuration());
return env->NewStringUTF(def.c_str());
}
Demo 源码:https://github.com/HJHL/FFMpegTurtorial