动手点关注 干货不迷路 👆
概述
本文首先以 FFmpeg 视频解码为主题,主要介绍了 FFmpeg 进行解码视频时的主要流程、基本原理;其次,文章还讲述了与 FFmpeg 视频解码有关的简单应用,包括如何在原有的 FFmpeg 视频解码的基础上按照一定时间轴顺序播放视频、如何在播放视频时加入 seek 的逻辑;除此之外,文章重点介绍了解码视频时可能容易遗漏的细节,最后是简单地阐述了下如何封装一个具有基本的视频解码功能的 VideoDecoder。
前言
FFmpeg
FFmpeg 是一套可以用来录制、转换数字音频、视频,并能将其转化为流的开源计算机程序,它可生成用于处理和操作多媒体数据的库,其中包含了先进的音视频解码库 libavcodec
和音视频格式转换库 libavformat
。
FFmpeg 六大常用功能模块
libavformat:多媒体文件或协议的封装和解封装库,如 mp4、flv 等文件封装格式,rtmp、rtsp 等网络协议封装格式;
libavcodec:音视频解码核心库;
libavfilter:音视频、字幕滤镜库;
libswscale:图像格式转换库;
libswresample:音频重采样库;
libavutil:工具库
视频解码基础入门
解复用(Demux):解复用也可叫解封装。这里有一个概念叫封装格式,封装格式指的是音视频的组合格式,常见的有 mp4、flv、mkv 等。通俗来讲,封装是将音频流、视频流、字幕流以及其他附件按一定规则组合成一个封装的产物。而解封装起着与封装相反的作用,将一个流媒体文件拆解成音频数据和视频数据等。此时拆分后数据是经过压缩编码的,常见的视频压缩数据格式有 h264。

解码(Decode):简单来说,就是对压缩的编码数据解压成原始的视频像素数据,常用的原始视频像素数据格式有 yuv。

色彩空间转换(Color Space Convert):通常对于图像显示器来说,它是通过 RGB 模型来显示图像的,但在传输图像数据时使用 YUV 模型可以节省带宽。因此在显示图像时就需要将 yuv 像素格式的数据转换成 rgb 的像素格式后再进行渲染。
渲染(Render):将前面已经解码和进行色彩空间转换的每一个视频帧的数据发送给显卡以绘制在屏幕画面上。
一、 引入 FFmpeg 前的准备工作
1.1 FFmpeg so 库编译
在 FFmpeg 官网下载源码库并解压;
下载 NDK 库并解压;
配置解压后的 FFmpeg 源码库目录中的 configure,修改高亮部分几个参数为以下的内容,主要目的是生成 Android 可使用的 名称-版本.so 文件的格式;
# ······
# build settings
SHFLAGS='-shared -Wl,-soname,$$(@F)'
LIBPREF="lib"
LIBSUF=".a"
FULLNAME='$(NAME)$(BUILDSUF)'
LIBNAME='$(LIBPREF)$(FULLNAME)$(LIBSUF)'
SLIBPREF="lib"
SLIBSUF=".so"
SLIBNAME='$(SLIBPREF)$(FULLNAME)$(SLIBSUF)'
SLIBNAME_WITH_VERSION='$(SLIBNAME).$(LIBVERSION)'
# 已修改配置
SLIBNAME_WITH_MAJOR='$(SLIBNAME)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
# ······
在 FFmpeg 源码库目录下新建脚本文件
build_android_arm_v8a.sh
,在文件中配置 NDK 的路径,并输入下面其他的内容;
# 清空上次的编译
make clean
# 这里先配置你的 NDK 路径
export NDK=/Users/bytedance/Library/Android/sdk/ndk/21.4.7075529
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64
function build_android
{
./configure \
--prefix=$PREFIX \
--disable-postproc \
--disable-debug \
--disable-doc \
--enable-FFmpeg \
--disable-doc \
--disable-symver \
--disable-static \
--enable-shared \
--cross-prefix=$CROSS_PREFIX \
--target-os=android \
--arch=$ARCH \
--cpu=$CPU \
--cc=$CC \
--cxx=$CXX \
--enable-cross-compile \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
--extra-ldflags="$ADDI_LDFLAGS"
make clean
make -j16
make install
echo "============================ build android arm64-v8a success =========================="
}
# arm64-v8a
ARCH=arm64
CPU=armv8-a
API=21
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-march=$CPU"
echo $CC
build_android
设置 NDK 文件夹中所有文件的权限
chmod 777 -R NDK
;终端执行脚本
./build_android_arm_v8a.sh
,开始编译 FFmpeg。编译成功后的文件会在 FFmpeg 下的android
目录中,会出现多个 .so 文件;

若要编译 arm-v7a,只需要拷贝修改以上的脚本为以下
build_android_arm_v7a.sh
的内容。
#armv7-a
ARCH=arm
CPU=armv7-a
API=21
CC=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang
CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-mfloat-abi=softfp -mfpu=vfp -marm -march=$CPU "
1.2 在 Android 中引入 FFmpeg 的 so 库
NDK 环境、CMake 构建工具、LLDB(C/C++ 代码调试工具);
新建 C++ module,一般会生成以下几个重要的文件:
CMakeLists.txt
、native-lib.cpp
、MainActivity
;在
app/src/main/
目录下,新建目录,并命名jniLibs
,这是 Android Studio 默认放置 so 动态库的目录;接着在jniLibs
目录下,新建arm64-v8a
目录,然后将编译好的 .so 文件粘贴至此目录下;然后再将编译时生成的 .h 头文件(FFmpeg 对外暴露的接口)粘贴至cpp
目录下的include
中。以上的 .so 动态库目录和 .h 头文件目录都会在CMakeLists.txt
中显式声明和链接进来;最上层的
MainActivity
,在这里面加载 C/C++ 代码编译的库:native-lib
。native-lib
在CMakeLists.txt
中被添加到名为 "ffmpeg" 的 library 中,所以在System.loadLibrary()
中输入的是 "ffmpeg";
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Example of a call to a native method
sample_text.text = stringFromJNI()
}
// 声明一个外部引用的方法,此方法和 C/C++ 层的代码是对应的。
external fun stringFromJNI(): String
companion object {
// 在 init{} 中加载 C/C++ 编译成的 library:ffmpeg
// library 名称的定义和添加在 CMakeLists.txt 中完成
init {
System.loadLibrary("ffmpeg")
}
}
}
native-lib.cpp
是一个 C++ 接口文件,Java 层中声明的 external 方法在这里得到实现;
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_bytedance_example_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CMakeLists.txt
是一个构建脚本,目的是配置可以编译出native-lib
此 so 库的构建信息;
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Declares and names the project.
project("ffmpeg")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
#