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就可以了。

目录图
编译的时候我们可以选择将所有库编译成.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

编译完以后输出如下

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的时候你可能会遇到一些问题,比如下面这样

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

image

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

FFmpeg会去调用openssl的一个函数来检查是否正确加入了openssl库,然而日志显示这个函数未定义。
这是因为openssl自1.1.0版本以后将此函数改为了OPENSSL_init_ssl。所以我们需要手动到FFmpeg的configure文件里面做一些改动,直接到configure中搜索SSL_library_init添加下面三行。
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。

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容