Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)

demo地址https://github.com/ColorfulHorse/learnFFmpeg, 包含编译脚本

本文主要参考https://github.com/byhook/ffmpeg4android以及雷霄骅博客

下一篇Android视音频开发初探【二】(简单的相机推流器)

一些概念

什么是音视频开发?

简单点说分为两个方面,一方面是播放视频的时候,要经历:解协议(网络视频,rtmp,rtsp)-> 解封装(flv, mp4) -> 解码(h.264,h.265) -> 得到rgb/yuv原始数据,音频得到pcm原数据,然后分别进行绘制和播放;
另一方面是录制视频的时候,流程刚好反过来:采集原数据 -> 原始数据编码 -> 音视频封装到一起 -> 协议封装传输(网络视频)。

关于各种格式和协议介绍更详细的知识可以看雷霄骅的这篇博客 视音频编解码技术零基础学习方法

为什么要进行编解码?

归根结底还是因为原数据太大,必须进行压缩。就拿我们熟悉的rgb565(一个像素2byte)来说,1080x1920 30fps的视频一秒钟需要1080x1920x2x30 = 118MB,这种量级是完全无法接受的,所以必然要进行压缩;对应图片有图片压缩算法,而视频由于动态连续性可以有更高压缩比的方法,这就是视频编码了。

比如h.264编码格式,就定义了I帧P帧B帧,I帧可以解析成一个完整的图像;P帧需要根据它之前的帧来补充自己的信息,本身存的信息比较少;B帧则需要通过前后帧来补充自己。关于H.264更详细一点的知识可以看这篇博客https://www.jianshu.com/p/1b3f8187b271

FFmpeg能做什么?

FFmpeg是一个纯c编写的开源库,提供了对音视频进行编解码、变换、采样、格式封装、滤镜后处理的一系列接口,由于统一了上层接口,开发者能更容易基于它开发兼容多平台的应用。视频编码标准迄今为止已经发展了好几代,每一个标准都有若干编解码器的实现,比如x264是最好的h.264编码器,把它接入到FFmpeg以后我们无需再关心x264的api,只需要通过FFmpeg提供的统一编码接口就可以使用它。

另外,编解码分为软编解码和硬编解码两种,软对应cpu,硬对应gpu,硬编解码虽然速度更快,但是并非所有设备都支持;软编解码效率较低占用资源也比较大,但是胜在兼容性,综合来说软硬结合比较合理。android中提供了硬编解码的api,java层对应MediaCodec类,使用FFmpeg添加mediacodec支持也可以在native层调用它,不过目前仅限于解码,还不支持编码。

编译FFmpeg

上面说了这么多,我们来开始第一步,编译FFmpeg。我的编译环境是虚拟机Ubuntu 20.04 LTS 桌面版,
ndkr21,ffmpeg4.2.3,x264最新版,fdk-aac2.0.1(音频编码),openssl1.1.1(用于添加https支持),对应的源码到官网去下载就好,需要注意的是ndk要下载linux版本。高版本的ndk已经移除了gcc,所以我们用clang来编译,同时arm架构只需要适配armv7和armv8就可以了。

Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)_第1张图片
目录图
编译的时候我们可以选择将所有库编译成.a静态库,然后把他们合并成一个.so动态库,但是这样的话最后的体积会比较大,我这里选择的是折衷的方式,将x264、fdkaac、openssl编译成静态库,然后动态编译FFmpeg生成多个so库。下面来编写编译脚本,这需要一点点shell知识。

定义公共变量

config.sh 用于初始化一些公用变量


#NDK路径
export ANDROID_NDK_ROOT=/home/lyj/dev/android-ndk-r21

export AOSP_API="21"

#cpu架构
if [ "$#" -lt 1 ]; then
    THE_ARCH=armv7
else
    THE_ARCH=$(tr [A-Z] [a-z] <<< "$1")
fi

#根据不同架构配置变量
case "$THE_ARCH" in
  armv7a|armeabi-v7a)
    TOOLNAME_BASE="arm-linux-androideabi"
    COMPILER_BASE="armv7a-linux-androideabi"
    AOSP_ABI="armeabi-v7a"
    AOSP_ARCH="armeabi-v7a"
    OPENSSL_ARCH="android-arm"
    HOST="arm-linux-androideabi"
    FF_EXTRA_CFLAGS="-DANDROID -Wall -fPIC"
    FF_CFLAGS="-DANDROID -Wall -fPIC"
    ;;
  armv8|armv8a|aarch64|arm64|arm64-v8a)
    TOOLNAME_BASE="aarch64-linux-android"
    COMPILER_BASE="aarch64-linux-android"
    AOSP_ABI="arm64-v8a"
    AOSP_ARCH="arm64"
    OPENSSL_ARCH="android-arm64"
    HOST="aarch64-linux-android"

    FF_EXTRA_CFLAGS="-DANDROID -Wall -fPIC"
    FF_CFLAGS="-DANDROID -Wall -fPIC"
    ;;
  x86)
    TOOLNAME_BASE="i686-linux-android"
    COMPILER_BASE="i686-linux-android"
    AOSP_ABI="x86"
    AOSP_ARCH="x86"
    OPENSSL_ARCH="android-x86"
    HOST="i686-linux-android"
    FF_EXTRA_CFLAGS="-DANDROID -Wall -fPIC"
    FF_CFLAGS="-DANDROID -Wall -fPIC"
    ;;
  x86_64|x64)
    TOOLNAME_BASE="x86_64-linux-android"
    COMPILER_BASE="x86_64-linux-android"
    AOSP_ABI="x86_64"
    AOSP_ARCH="x86_64"
    OPENSSL_ARCH="android-x86_64"
    HOST="x86_64-linux-android"
    FF_EXTRA_CFLAGS="-DANDROID -Wall -fPIC"
    FF_CFLAGS="-DANDROID -Wall -fPIC"
    ;;
  *)
    echo "ERROR: Unknown architecture $1"
    [ "$0" = "$BASH_SOURCE" ] && exit 1 || return 1
    ;;
esac
# 工具链
TOOLCHAIN=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64
SYS_ROOT=$TOOLCHAIN/sysroot
# 交叉编译路径
CROSS_PREFIX=$TOOLCHAIN/bin/$TOOLNAME_BASE-
# 编译器
CC=$TOOLCHAIN/bin/$COMPILER_BASE$AOSP_API-clang
CXX=$TOOLCHAIN/bin/$COMPILER_BASE$AOSP_API-clang++

echo "TOOLNAME_BASE="$TOOLNAME_BASE
echo "COMPILER_BASE="$COMPILER_BASE
echo "AOSP_ABI="$AOSP_ABI
echo "AOSP_ARCH="$AOSP_ARCH
echo "HOST="$HOST

编译x264

build_x264.sh

#!/bin/bash

ARCH=$1

# 导入配置文件
source config.sh $ARCH
# 输出路径
LIBS_DIR=$(cd `dirname $0`; pwd)/libs/libx264
echo "LIBS_DIR="$LIBS_DIR

# x264源码路径
cd x264

export CC=$CC
export CXX=$CXX
export CXXFLAGS=$FF_EXTRA_CFLAGS
export CFLAGS=$FF_CFLAGS
export AR="${CROSS_PREFIX}ar"
export LD="${CROSS_PREFIX}ld"
export AS="${CROSS_PREFIX}as"
export NM="${CROSS_COMPILE}nm"
export STRIP="${CROSS_COMPILE}strip"
export RANLIB="${CROSS_COMPILE}ranlib"

PREFIX=$LIBS_DIR/$AOSP_ABI

./configure --prefix=$PREFIX \
--enable-static \
--enable-pic \
--disable-cli \
--disable-asm \
--host=$HOST \
--cross-prefix=$CROSS_PREFIX \
--sysroot=$SYS_ROOT \
--extra-cflags="$FF_CFLAGS" \
--extra-ldflags=""

make clean
make -j2
make install

cd ..

命令行sudo bash buid_x264.sh armv7a 编译对应平台,同时我们也可以写一个shell一次性编译所有平台,其他的脚本也可以参照这个做法
build_x264_all.sh

for arch in armeabi-v7a arm64-v8a x86 x86_64
do
    bash build_x264.sh $arch
done

编译完以后输出如下

Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)_第2张图片
image

编译fdk-aac

build_fdkaac.sh

#!/bin/bash

ARCH=$1

source config.sh $ARCH
LIBS_DIR=$(cd `dirname $0`; pwd)/libs/libfdk-aac

cd fdk-aac-2.0.1


PREFIX=$LIBS_DIR/$AOSP_ABI
echo "PREFIX="$PREFIX

export CC="$CC"
export CXX="$CXX"
export CFLAGS="$FF_CFLAGS"
export CXXFLAGS="$FF_EXTRA_CFLAGS"
# x86架构源码中使用了math库所以必须链接
export LDFLAGS="-lm"
export AR="${CROSS_PREFIX}ar"
export LD="${CROSS_PREFIX}ld"
export AS="${CROSS_PREFIX}as"


./configure \
--prefix=$PREFIX \
--target=android \
--with-sysroot=$SYS_ROOT \
--enable-static \
--disable-shared \
--host=$HOST 


make clean
make -j2
make install

cd ..

编译fdkaac的时候你可能会遇到一些问题,比如下面这样

Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)_第3张图片
fdkaac-error
这是因为fdk-aac已经作为android的一部分被构建了,所以直接引入了android才有的log库打印一些日志,我们可以到/libSBRdec/src/llp_tran.cpp源码中把log相关代码删掉,有几个地方需要删。本方法来自issue区作者的回答

tag1

编译openssl

openssl脚本: build_openssl.sh

#!/bin/bash
ARCH=$1
source config.sh $ARCH
LIBS_DIR=$(cd `dirname $0`; pwd)/libs/openssl
PREFIX=$LIBS_DIR/$AOSP_ABI
echo "PREFIX"=$PREFIX

cd openssl
export ANDROID_NDK_HOME=$ANDROID_NDK_ROOT
export PATH=$TOOLCHAIN/bin:$PATH
export CC="$CC"
export CXX="$CXX"
export AR="${CROSS_PREFIX}ar"
export LD="${CROSS_PREFIX}ld"
export AS="${CROSS_PREFIX}as"
export NM="${CROSS_COMPILE}nm"

./Configure $OPENSSL_ARCH \
-D__ANDROID_API__=$AOSP_API \
--prefix=$PREFIX \
no-shared \
no-engine \
no-dtls \
no-hw

make clean
make -j2
make install

cd ..

编译FFmpeg

源码包里面有一个configure文件,我们可以./configure --help来查看编译配置,也可以去看文档
FFmpeg wiki
下面的脚本这么多配置是为了对库进行按需裁剪,可以根据自己的实际情况改动

注意编译的时候要将 \ 后面的注释全部去掉,不然无法解析

build_ffmpeg.sh

#!/bin/bash
# 编译ffmpeg,链接x264和fdkaac
ARCH=$1

source config.sh $ARCH
NOW_DIR=$(cd `dirname $0`; pwd)
LIBS_DIR=$NOW_DIR/libs

# 源码目录,自行更改
cd ffmpeg-4.2.3


# 输出路径
PREFIX=$LIBS_DIR/ffmpeg/$AOSP_ABI

# 头文件目录
FDK_INCLUDE=$LIBS_DIR/libfdk-aac/$AOSP_ABI/include
# 库文件目录
FDK_LIB=$LIBS_DIR/libfdk-aac/$AOSP_ABI/lib
X264_INCLUDE=$LIBS_DIR/libx264/$AOSP_ABI/include
X264_LIB=$LIBS_DIR/libx264/$AOSP_ABI/lib
OPENSSL_INCLUDE=$LIBS_DIR/openssl/$AOSP_ABI/include
OPENSSL_LIB=$LIBS_DIR/openssl/$AOSP_ABI/lib

./configure \
--target-os=android \
--prefix=$PREFIX \
--enable-cross-compile \
--disable-runtime-cpudetect \
--disable-asm \
--arch=$AOSP_ARCH \
--cc=$CC \
--cxx=$CXX \
--cross-prefix=$CROSS_PREFIX \
# 链接头文件路径
--extra-cflags="-I$X264_INCLUDE  -I$FDK_INCLUDE -I$OPENSSL_INCLUDE $FF_CFLAGS" \
--extra-cxxflags="$FF_EXTRA_CFLAGS" \
# 链接库文件路径
--extra-ldflags="-L$X264_LIB -L$FDK_LIB -L$OPENSSL_LIB" \
--extra-libs=-lm \
--sysroot=$SYS_ROOT \
--disable-static \
--enable-shared \
--enable-jni \
--enable-mediacodec \
--enable-pthreads \
--enable-pic \
--disable-iconv \
--enable-libx264 \
--enable-libfdk_aac \
--enable-openssl \
--enable-gpl \
--enable-nonfree \
# 编复用器,用于格式封装
--disable-muxers \
--enable-muxer=mov \
--enable-muxer=mp4 \
--enable-muxer=h264 \
--enable-muxer=avi \
--enable-muxer=flv \
--enable-muxer=hls \
--enable-muxer=rtp \
--enable-muxer=rtsp \
# 解复用器
--disable-demuxers \
--enable-demuxer=mov \
--enable-demuxer=h264 \
--enable-demuxer=avi \
--enable-demuxer=flv \
--enable-demuxer=hls \
--enable-demuxer=rtp \
--enable-demuxer=rtsp \
# 编码器
--disable-encoders \
--enable-encoder=aac \
--enable-encoder=libfdk_aac \
--enable-encoder=libx264 \
--enable-encoder=mpeg4 \
--enable-encoder=mjpeg \
--enable-encoder=png \
# 解码器
--disable-decoders \
--enable-decoder=aac \
--enable-decoder=aac_latm \
--enable-decoder=libfdk_aac \
--enable-decoder=h264 \
--enable-decoder=h264_mediacodec \
--enable-decoder=mpeg4 \
--enable-decoder=mjpeg \
--enable-decoder=png \
#解析器
--disable-parsers \
--enable-parser=aac \
--enable-parser=aac_latm \
--enable-parser=h264 \
--enable-parser=mjpeg \
--enable-parser=png \
# 传输协议
--disable-protocols \
--enable-protocol=file \
--enable-protocol=crypto \
--enable-protocol=http \
--enable-protocol=https \
--enable-protocol=tls \
--enable-protocol=tcp \
--enable-protocol=udp \
--enable-protocol=rtp \
--enable-protocol=rtmp \
--enable-protocol=rtmps \
--enable-protocol=hls \
# 其他
--enable-zlib \
--enable-small \
--enable-postproc \
--disable-outdevs \
--disable-indevs \
--disable-ffprobe \
--disable-ffplay \
--disable-ffmpeg \
--disable-debug \
--disable-symver 
make clean
make -j2
make install

cd ..

如果加入了openssl也许你会碰到下图的情况,提示openssl not found

Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)_第4张图片
image

我们到FFmpeg源码目录/ffbuild/config.log看一下编译日志
image

FFmpeg会去调用openssl的一个函数来检查是否正确加入了openssl库,然而日志显示这个函数未定义。
这是因为openssl自1.1.0版本以后将此函数改为了OPENSSL_init_ssl。所以我们需要手动到FFmpeg的configure文件里面做一些改动,直接到configure中搜索SSL_library_init添加下面三行。
Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)_第5张图片
image

check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto ||
check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl32 -leay32 ||
check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto -lws2_32 -lgdi32 ||

之后再重新编译就没有问题了,之后将FFmpeg输出目录中各个平台的头文件和库文件拷贝到项目中就可以了,下一篇博客我们来学习如何使用FFmpeg。

Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl)_第6张图片
image

你可能感兴趣的:(Android视音频开发初探【一】(clang编译FFmpeg+fdk-aac+x264+openssl))