JasonWang's Blog

如何用FFmpeg在Android上实现音视频解码

字数统计: 2.3k阅读时长: 11 min
2022/09/30

最近开发投屏功能,需要对H.264视频数据流进行解码,然后显示出来。Android原生的MediaCodec虽然使用了硬件解码,但是延迟较大(超过300ms),无法满足要求。于是研究了下如何基于FFMPEG来做视频流的软解码。这里对整个过程做简要的总结,看下如何在Android Studio中完成FFMPEG的视频解码:

  • 简单介绍下FFMPEG框架
  • 如何利用交叉编译生成所需要的FFMPEG共享库, 以及如何进行Android Studio的配置
  • FFMPEG解码H264的大致调用流程

FFMPEG框架介绍

FFmpeg(Fast Forward mpeg)是音视频编解码开源框架的标杆,支持常见音视频格式的编解码,如H.264/AAC/H.265/MP4等,也支持不同格式之间的转码,同时还支持流媒体协议如RTSP/RTMP。目前,不少视频网站如Youtube/Bilibili等都是通过FFmpeg来实现音视频的处理的。

按功能模块划分,FFmpeg大致分为如下几个部分:

  • libavcodec: 包含了所有音视频编解码的核心代码
  • libavdevice: 用于操作内部、外部音视频设备,以达到硬件加速/显示/加速等功能
  • libavfilter: 音视频滤波器的开发,如宽高比、裁剪、格式化、非格式化、伸缩等
  • libavformat: 用于解析各种不同的音视频封装格式
  • libavutil: 包含公共的工具函数,如算术运算、字符操作等
  • libswresample: 原始音频格式转码
  • libswscale: 用于视频场景比例缩放、色彩映射转换、图像颜色空间或格式转换,如RGB565/RGB888等与 YUV420等之间的转换。

同时,FFmpeg源码中包含了ffprobe(用于分析音视频数据流)/ffplay(基于SDL的播放器)/ffmpeg(视频转换工具)等常用的工具。初次使用FFmepg接口时,可以参考源码中的示例doc/examples, 里面给出了很多常见接口的使用方法。

更多有关FFmpeg的资料,可以参考:

下面我们就来看下如何在Ubuntu环境中交叉编译FFmpeg到基于Android的ARM64平台架构上。

我使用的编译环境是 Ubuntu 18.04 + Android ndk 22.1.7171670

交叉编译FFmpeg到Android平台

首先通过git下载FFmpeg源码, 为确保功能稳定,编译使用的是最近发布版本分支release/5.1的代码:

1
2
3
4
5
6

git clone https://github.com/FFmpeg/FFmpeg

# 切换到5.1分支
git checkout -b rel_5.1 origin/release/5.1

下载完后,可以通过./configure -h来查看各种编译配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

Usage: configure [options]
Options: [defaults in brackets after descriptions]

Help options:
--help print this message
--quiet Suppress showing informative output
--list-decoders show all available decoders
--list-encoders show all available encoders
--list-hwaccels show all available hardware accelerators
--list-demuxers show all available demuxers
--list-muxers show all available muxers
--list-parsers show all available parsers
--list-protocols show all available protocols
--list-bsfs show all available bitstream filters
--list-indevs show all available input devices
--list-outdevs show all available output devices
--list-filters show all available filters

Standard options:
--logfile=FILE log tests and output to FILE [ffbuild/config.log]
--disable-logging do not log configure debug information
--fatal-warnings fail if any configure warning is generated
--prefix=PREFIX install in PREFIX [/usr/local]
--bindir=DIR install binaries in DIR [PREFIX/bin]
--datadir=DIR install data files in DIR [PREFIX/share/ffmpeg]
--docdir=DIR install documentation in DIR [PREFIX/share/doc/ffmpeg]
--libdir=DIR install libs in DIR [PREFIX/lib]
--shlibdir=DIR install shared libs in DIR [LIBDIR]
--incdir=DIR install includes in DIR [PREFIX/include]
--mandir=DIR install man page in DIR [PREFIX/share/man]
--pkgconfigdir=DIR install pkg-config files in DIR [LIBDIR/pkgconfig]
--enable-rpath use rpath to allow installing libraries in paths
not part of the dynamic linker search path
use rpath when linking programs (USE WITH CARE)
--install-name-dir=DIR Darwin directory name for installed targets

Licensing options:
--enable-gpl allow use of GPL code, the resulting libs
and binaries will be under GPL [no]
--enable-version3 upgrade (L)GPL to version 3 [no]
--enable-nonfree allow use of nonfree code, the resulting libs
and binaries will be unredistributable [no]

Configuration options:
--disable-static do not build static libraries [no]
--enable-shared build shared libraries [no]
--enable-small optimize for size instead of speed
--disable-runtime-cpudetect disable detecting CPU capabilities at runtime (smaller binary)
--enable-gray enable full grayscale support (slower color)
--disable-swscale-alpha disable alpha channel support in swscale
--disable-all disable building components, libraries and programs
--disable-autodetect disable automatically detected external libraries [no]

接下来,为了便于编译,我们需要写一个编译脚本build_android.sh,用于配置交叉编译的参数与环境变量(这里只编译了64位系统的库,如果要编译32位的库,修改下ARCH、CPU变量即可: ARCH=arm; CPU=armv7-a):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

#!/bin/bash
export NDK=/xxx/AndroidSDK/ndk/22.1.7171670
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64

function build_android
{
./configure \
--prefix=$PREFIX \
--disable-postproc \
--disable-debug \
--disable-symver \
--disable-static \
--enable-shared \
--disable-doc \
--disable-ffmpeg \
--cross-prefix=$CROSS_PREFIX \
--target-os=android \
--arch=$ARCH \
--cpu=$CPU \
--cc=$CC \
--cxx=$CXX \
--enable-cross-compile \
--enable-gpl \
--sysroot=$SYSROOT \
--extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS" \
#--extra-ldflags="$ADDI_LDFLAGS"

make clean
make -j4
make install

echo "============== build android arm64-v8a success =============="

}

# arm64-v8a
ARCH=arm64
CPU=armv8-a
API=26
CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang
CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++
SYSROOT=$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
PREFIX=$(pwd)/android/$CPU
OPTIMIZE_CFLAGS="-march=$CPU"
ADDI_LDFLAGS="LDFLAGS='-Wl,-z,relro -Wl,-z,now -pie"

echo $CC

build_android

执行chmod a+x ./build_android.sh; ./build_android.sh就开始了编译。编译成功后,会有一个android/armv8-a的文件夹,里边包含了交叉编译生成的静态与动态库:

1
2
3
4
5
6
7
8

# ./android/armv8-a

bin --> 常用的工具,如`ffmpeg`, `ffprobe`
lib --> 共享库,如`libavcodec`,`libavformat`, `libswscale`等
include --> 包含所有开发所需的头文件
share --> 包含了相关示例与文档

实际开发时,我们只需要用到include/lib两个目录中的文件即可。

将共享库集成到Android Studio

最开始配置的时候,把编译好的库放到src/main/jniLibs/armv8-a目录下面,编译虽然正常,但是运行时却找不到对应的库。只好新建一个目录libs将头文件跟预编译的库放在这里(参考配置CMAKE):

FFmpeg config

然后在CMakeLists.txt里添加对应的库与头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

set(ffmpeg_lib_dir ../../../../libs/arm64-v8a)
set(ffmpeg_head_dir ../../../../libs/arm64-v8a/include)

add_library( avcodec
SHARED
IMPORTED)
set_target_properties(avcodec
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavcodec.so)

add_library( avformat
SHARED
IMPORTED)
set_target_properties(avformat
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavformat.so)

add_library( avutil
SHARED
IMPORTED)
set_target_properties(avutil
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libavutil.so)

add_library( swscale
SHARED
IMPORTED)
set_target_properties(swscale
PROPERTIES IMPORTED_LOCATION
${ffmpeg_lib_dir}/libswscale.so)
...

include_directories(libs/arm64-v8a/include)

开发使用的Android Studio的版本是4.2.1

配置完成就可以基于FFmpeg的接口开发了。下面我们简单来看看如何用FFmpeg解码H264的视频流。

FFmpeg解码H264视频流

关于如何利用FFmpeg来解码音视频文件,网络上有很多参考资料了,比如:

解码从网络端接收到的视频流,大致流程基本一致, 主要分为几个关键的步骤:

  • 初始化FFmpeg解码器, 如找到对应的解码器,配置解码器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

mVideoDecoder = avcodec_find_decoder(AV_CODEC_ID_H264);
if (mVideoDecoder == nullptr) {
LOGE("%s: fail to find h264 decoder", __func__ );
return false;
}

mVCodecCtx = avcodec_alloc_context3(mVideoDecoder);
if (mVCodecCtx == nullptr) {
LOGE("%s: fail to create codec context", __func__ );
return false;
}

mVCodecCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
//mVCodecCtx->flags |= AV_CODEC_FLAG2_FAST;

mVCodecCtx->width = mVideoW;
mVCodecCtx->height = mVideoH;
mVCodecCtx->bit_rate = IDecoder::BIT_RATE;
mVCodecCtx->framerate = av_make_q(IDecoder::FRAME_RATE, 1);

if (avcodec_open2(mVCodecCtx, mVideoDecoder, nullptr) < 0) {
LOGE("%s: Fail to open codec: %s", __func__ , strerror(errno));
avcodec_free_context(&mVCodecCtx);
return false;
}

  • 将接收到的H264数据流分片NAL(Network Abstraction Layer)后,放入队列
  • 解码线程从队列中取出数据包,然后解码;解码的过程大致分为四个步骤:
    • 发送待解码的数据报给解码器avcodec_send_packet
    • 从解码器接收解码后的包avcodec_receive_frame
    • 将解码的包从YUV格式转换为RGB格式
    • RGB格式的视频帧拷贝到Surface进行渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

bool FFmpegDecoder::doDecode(h264_decode_struct *packet) {
AVPacket *raw_packet = mDecoderResources->avPacket;
if (raw_packet != nullptr) {
raw_packet->data = packet->data;
raw_packet->size = packet->data_len;
raw_packet->pts = packet->pts;
/* send raw packet to decode */
int res = avcodec_send_packet(mCodecCtx, raw_packet);
if (res < 0 && res != AVERROR(EAGAIN)) {
LOGE("%s: Could not send video packet", __func__ );
return false;
}

/* decode frame */
AVFrame *frame = mDecoderResources->decodeFrame;
res = avcodec_receive_frame(mCodecCtx, frame);
if (!res) {
LOGD("%s: decoded frame pts = %lld, pixel-format = %d, picture-type = %d", __func__ , (long long)frame->pts, frame->format, frame->pict_type);
if (frame->width != mVideoW || frame->height != mVideoH) {
LOGE("%s: video size changed, drop frame", __func__ );
return true;
}

/*
* render to Surface
* decoded frame is YUV format which need to transform to RGB before rendering
*/
AVFrame *rgb_frame = mDecoderResources->rgbFrame;

av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, mDecodeOutBuf, AV_PIX_FMT_RGBA,
mVideoW, mVideoH, 1);

struct SwsContext *data_convert_ctx = sws_getContext(mVideoW, mVideoH, mCodecCtx->pix_fmt,
mVideoW, mVideoH, AV_PIX_FMT_RGBA,
SWS_BICUBIC, nullptr, nullptr, nullptr);
/* from YUV to RGA */
res = sws_scale(data_convert_ctx, (const uint8_t * const *) frame->data, frame->linesize, 0,
mVideoH, rgb_frame->data, rgb_frame->linesize);
if (res < 0) {
sws_freeContext(data_convert_ctx);
LOGE("%s: Fail to scale frame : %s", __func__ , strerror(errno));
return false;
} else {
/* render to screen */
res = ANativeWindow_lock(mNativeWin, mWindowBuf, nullptr);
if (res < 0) {
LOGE("%s: Fail to lock window", __func__ );
} else {
uint8_t *bits = (uint8_t *) mWindowBuf->bits;
for (int i = 0; i < mVideoH; ++i) {
memcpy(bits + i * mWindowBuf->stride * 4,
mDecodeOutBuf + i * rgb_frame->linesize[0],
rgb_frame->linesize[0]);
}
ANativeWindow_unlockAndPost(mNativeWin);
}
}

sws_freeContext(data_convert_ctx);

return true;
}

LOGE("%s: fail to receive frame %d", __func__ , res);
return false;
}
}


相比Android原生的MediaCodec硬解码,FFmpeg解码效率提升了很多,延迟从原来的400+ms减少到了100ms左右,改善明显。但是由于使用了CPU进行解码操作,系统的负载与CPU使用率都会有所升高。因此,在进行高清视频的解码时硬解码会更合适。

总结下来,FFmpeg框架确实十分强大,也有比较完善的生态社区,可以说是搞音视频开发必不可少的利器。

参考文献

CATALOG
  1. 1. FFMPEG框架介绍
  2. 2. 交叉编译FFmpeg到Android平台
  3. 3. 将共享库集成到Android Studio
  4. 4. FFmpeg解码H264视频流
  5. 5. 参考文献