ffmpeg使用drawtext滤镜需要在编译的时候使能drawtext,要想成功使能必须要先集成编译freetype库,并通知到ffmpeg(交叉编译没有安装到系统库路径)。
也有看到有的文章说需要集成fribidi,笔者也交叉编译了,但是最终没有用到,可能是ffmpeg版本的原因。
一、交叉编译freetype
freetype:下载地址:https://freetype.org/download.html
笔者选择了红框链接进行下载,下载的2.10版本。
(ps:下载的时候我没注意时间,以为最上面的是最新的【笑哭】)
下载完成后解压文件(解压到当前文件夹)
tar -zxvf freetype-2.10.0.tar.gz .
接下来就是交叉编译了,没有交叉编译环境的朋友可以参考我之前的文章搭建环境,传送门Android FFMPEG编解码实践(一):Ubuntu 22.04 NDK编译FFMPEG+libx264_android编译libx264-CSDN博客
新建shell脚本文件,文件内容如下。笔者使用的是ubuntu的虚拟机,所以工具链选择的是linux-x86_64,其他的大家根据自己的环境路径修改一下DNK的路径。
#!/bin/bash
export NDK=/home/selivert/ndk/android-ndk-r21e #NDK path
TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64
export API=21
function build_android
{
APP_ABI=$1
echo "======== > Start build $APP_ABI"
case ${APP_ABI} in
armeabi-v7a)
HOST=armv7a-linux-android
CROSS_PREFIX=$TOOLCHAIN/bin/arm-linux-androideabi-
;;
arm64-v8a)
HOST=aarch64-linux-android
CROSS_PREFIX=$TOOLCHAIN/bin/aarch64-linux-android-
;;
esac
./configure \
--prefix=$PREFIX \
--disable-cli \
--enable-shared \
--with-pic \
--enable-static \
--enable-strip \
--host=$HOST
make clean
make -j4
make install
}
PREFIX=`pwd`/android/armeabi-v7a
SYSROOT=$TOOLCHAIN/sysroot
export TARGET=armv7a-linux-androideabi
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
#export EXTRA_CFLAGS="-O0 -g"
#export EXTRA_CXXFLAGS="-O0 -g"
build_android armeabi-v7a
PREFIX=`pwd`/android/arm64-v8a
export TARGET=aarch64-linux-android
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
build_android arm64-v8a
上面的脚本同时编译了32位与64的库。
执行脚本,编译完成后(笔者过程很顺利,一次过),我们到输出目录下查看结果:
pkgconfig和库文件都有了,编译完成。将结果拷贝到外部文件夹,方便ffmpeg集中引用。
笔者放在用户目录下的out文件夹中,区分了32位与64位。
32位目录结构如下,内部包含了fdk-aac x264 x265等第三方开源库
二、编译ffmpeg 并使能drawtext滤镜
ffmpeg资源下载不再赘述,参考之前的文章。笔者使用的是4.4.2版本(这个版本官网好像找不到了,4.4.3应该也没问题)。
不多说,直接上编译脚本 ffmpeg_build_android.sh,注意ndk路径换成自己的(tag1)。如果不需要编码功能,注释掉(tag2)
#!/bin/bash
# 用于编译android平台的脚本
# NDK所在目录
NDK_PATH=/home/selivert/ndk/android-ndk-r21e # tag1
HOST_PLATFORM=linux-x86_64
# minSdkVersion
API=21
TOOLCHAINS="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM"
SYSROOT="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_PLATFORM/sysroot"
# 生成 -fpic 与位置无关的代码
CFLAG="-D__ANDROID_API__=$API -Os -fPIC -DANDROID "
LDFLAG="-lc -lm -ldl -llog "
# 输出目录
PREFIX=`pwd`/android
# 日志输出目录
CONFIG_LOG_PATH=${PREFIX}/log
# 公共配置
COMMON_OPTIONS=
# 交叉配置
CONFIGURATION=
build() {
APP_ABI=$1
CFLAGTHIRD=-I/home/selivert/out/$APP_ABI/include
LDFLAGTHIRD=-L/home/selivert/out/$APP_ABI/lib
echo "======== > Start build $APP_ABI"
case ${APP_ABI} in
armeabi-v7a)
ARCH="arm"
CPU="armv7-a"
MARCH="armv7-a"
TARGET=armv7a-linux-androideabi
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
# 交叉编译工具前缀
CROSS_PREFIX="$TOOLCHAINS/bin/arm-linux-androideabi-"
EXTRA_CFLAGS="$CFLAG -mfloat-abi=softfp -mfpu=vfp -marm -march=$MARCH "
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS="--enable-neon --cpu=$CPU "
;;
arm64-v8a)
ARCH="aarch64"
TARGET=$ARCH-linux-android
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
EXTRA_CFLAGS="$CFLAG"
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS=""
;;
x86)
ARCH="x86"
CPU="i686"
MARCH="i686"
TARGET=i686-linux-android
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
#EXTRA_CFLAGS="$CFLAG -march=$MARCH -mtune=intel -mssse3 -mfpmath=sse -m32"
EXTRA_CFLAGS="$CFLAG -march=$MARCH -mssse3 -mfpmath=sse -m32 "
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS="--cpu=$CPU "
;;
x86_64)
ARCH="x86_64"
CPU="x86-64"
MARCH="x86_64"
TARGET=$ARCH-linux-android
CC="$TOOLCHAINS/bin/$TARGET$API-clang"
CXX="$TOOLCHAINS/bin/$TARGET$API-clang++"
LD="$TOOLCHAINS/bin/$TARGET$API-clang"
CROSS_PREFIX="$TOOLCHAINS/bin/$TARGET-"
#EXTRA_CFLAGS="$CFLAG -march=$CPU -mtune=intel -msse4.2 -mpopcnt -m64"
EXTRA_CFLAGS="$CFLAG -march=$CPU -msse4.2 -mpopcnt -m64 "
EXTRA_LDFLAGS="$LDFLAG"
EXTRA_OPTIONS="--cpu=$CPU "
;;
esac
echo "-------- > Start clean workspace"
make clean
echo "-------- > Start build configuration"
CONFIGURATION="$COMMON_OPTIONS"
CONFIGURATION="$CONFIGURATION --logfile=$CONFIG_LOG_PATH/config_$APP_ABI.log"
CONFIGURATION="$CONFIGURATION --prefix=$PREFIX"
CONFIGURATION="$CONFIGURATION --libdir=$PREFIX/libs/$APP_ABI"
CONFIGURATION="$CONFIGURATION --incdir=$PREFIX/includes/$APP_ABI"
CONFIGURATION="$CONFIGURATION --pkgconfigdir=$PREFIX/pkgconfig/$APP_ABI"
CONFIGURATION="$CONFIGURATION --cross-prefix=$CROSS_PREFIX"
CONFIGURATION="$CONFIGURATION --arch=$ARCH"
CONFIGURATION="$CONFIGURATION --sysroot=$SYSROOT"
CONFIGURATION="$CONFIGURATION --cc=$CC"
CONFIGURATION="$CONFIGURATION --cxx=$CXX"
CONFIGURATION="$CONFIGURATION --ld=$LD"
# nm 和 strip
CONFIGURATION="$CONFIGURATION --nm=$TOOLCHAINS/bin/llvm-nm"
CONFIGURATION="$CONFIGURATION --strip=$TOOLCHAINS/bin/llvm-strip"
CONFIGURATION="$CONFIGURATION $EXTRA_OPTIONS"
echo "-------- > Start config makefile with $CONFIGURATION --extra-cflags=${EXTRA_CFLAGS}${CFLAG264} --extra-ldflags=${EXTRA_LDFLAGS}${LDFLAG264}"
./configure ${CONFIGURATION} \
--pkg-config="pkg-config --static" \
--extra-cflags="$EXTRA_CFLAGS$CFLAGTHIRD" \
--extra-ldflags="$EXTRA_LDFLAGS$LDFLAGTHIRD"
#exit
echo "-------- > Start make $APP_ABI with -j1"
make -j1
echo "-------- > Start install $APP_ABI"
make install
echo "++++++++ > make and install $APP_ABI complete."
}
build_all() {
#配置开源协议声明
COMMON_OPTIONS="$COMMON_OPTIONS --enable-gpl"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-nonfree"
#目标android平台
COMMON_OPTIONS="$COMMON_OPTIONS --target-os=android"
#配置动态库 静态库静
COMMON_OPTIONS="$COMMON_OPTIONS --enable-static"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-shared"
#COMMON_OPTIONS="$COMMON_OPTIONS --disable-static"
#COMMON_OPTIONS="$COMMON_OPTIONS --disable-shared"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-protocols"
#开启交叉编译
COMMON_OPTIONS="$COMMON_OPTIONS --enable-cross-compile"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-optimizations"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-debug"
#尽可能小
COMMON_OPTIONS="$COMMON_OPTIONS --enable-small"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-doc"
#不要命令(执行文件)
COMMON_OPTIONS="$COMMON_OPTIONS --disable-programs" # do not build command line programs
COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffmpeg" # disable ffmpeg build
COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffplay" # disable ffplay build
COMMON_OPTIONS="$COMMON_OPTIONS --disable-ffprobe" # disable ffprobe build
COMMON_OPTIONS="$COMMON_OPTIONS --disable-symver"
#COMMON_OPTIONS="$COMMON_OPTIONS --disable-network"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-x86asm"
COMMON_OPTIONS="$COMMON_OPTIONS --disable-asm"
#启用
COMMON_OPTIONS="$COMMON_OPTIONS --enable-pthreads"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-mediacodec"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-jni"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-zlib"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-pic"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-libx264" #tag2
COMMON_OPTIONS="$COMMON_OPTIONS --enable-libfdk-aac" #tag2
COMMON_OPTIONS="$COMMON_OPTIONS --enable-libfreetype"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-encoder=libfdk_aac" #tag2
COMMON_OPTIONS="$COMMON_OPTIONS --enable-libx265" #tag2
#COMMON_OPTIONS="$COMMON_OPTIONS --enable-avresample"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-encoder=libx264" #tag2
COMMON_OPTIONS="$COMMON_OPTIONS --enable-muxer=flv"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mpeg4"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=mjpeg"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=png"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=vorbis"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=opus"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-decoder=flac"
COMMON_OPTIONS="$COMMON_OPTIONS --enable-filter=drawtext"
echo "COMMON_OPTIONS=$COMMON_OPTIONS"
echo "PREFIX=$PREFIX"
echo "CONFIG_LOG_PATH=$CONFIG_LOG_PATH"
mkdir -p ${CONFIG_LOG_PATH}
#build "armeabi-v7a"
build "arm64-v8a"
#build "x86"
#build "x86_64"
}
#export PKG_CONFIG_PATH=/home/selivert/out/armeabi-v7a/lib/pkgconfig
echo "-------- Start --------"
build_all
echo "-------- End --------"
需要注意的是,笔者shell脚本的基本功基本等于0,也没有将32位与64位分成两个脚本,编译32位与64的库时需要手动修改脚本。
编译32位:
上述脚本注释build "arm64-v8a",放开build "armeabi-v7a"保存后,在命令行执行如下命令
export PKG_CONFIG_PATH=/home/selivert/out/armeabi-v7a/lib/pkgconfig
./ffmpeg_build_android.sh
编译64位:
上述脚本放开build "arm64-v8a", 注释build "armeabi-v7a"保存后,在命令行执行如下命令
export PKG_CONFIG_PATH=/home/selivert/out/arm64-v8a/lib/pkgconfig
./ffmpeg_build_android.sh
编译完成,到android目录下,将32位与64位的库与头文件拷贝出来,为android项目集成做准备。
PS:头文件拷贝ffmpeg的即可,库文件除了ffmpeg的so文件,还需拷贝依赖的第三方库文件(如libfreetype.so)。
三、代码使用drawtext
使用drawtext主要是初始化滤镜与使用滤镜。直接上伪代码。
初始化:需要注意的是字体文件(fontfile)需要给全路径。
AVFilterGraph * filter_graph = NULL;
AVFilterContext *bufferSinkCtx = NULL;
AVFilterContext *bufferSrcCtx = NULL;
AVFilterContext* filterCtx = NULL;
int initFilter(AVCodecContext * codecContext)
{
char args[512] = {0};
int ret = 0;
int nb_filters = 0;
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
char filters_descr[2048] = {0};
char font[] = {"/system/fonts/NotoSansCJK-Regular.ttc"}; // 绘制文字的字体,安卓系统中文字体(中日韩)
int fontSize = 36; // 绘制文字的大小 pix
int yLocation = codecContext->height - fontSize * 3.5;
sprintf(filters_descr,"drawtext=fontfile='%s':fontcolor=white:fontsize=%d:text='hello ffmpeg':x=(w-text_w)/2:y=%d",font, fontSize, yLocation); // 文字居中在视频底部
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
filter_graph = avfilter_graph_alloc();
if (!outputs || !inputs || !filter_graph) {
ret = AVERROR(ENOMEM);
LOGE("initFilter ENOMEM \n");
goto end;
}
/* buffer video source: the decoded frames from the decoder will be inserted here. */
sprintf(args,
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
codecContext->width, codecContext->height, codecContext->pix_fmt,
codecContext->time_base.num, codecContext->time_base.den,
codecContext->sample_aspect_ratio.num, codecContext->sample_aspect_ratio.den);
LOGE("initFilter: %s\n", args);
ret = avfilter_graph_create_filter(&bufferSrcCtx, buffersrc, "in",
args, NULL, filter_graph);
if (ret < 0) {
LOGE("Cannot create buffer source\n");
goto end;
}
/* buffer video sink: to terminate the filter chain. */
ret = avfilter_graph_create_filter(&bufferSinkCtx, buffersink, "out",
NULL, NULL, filter_graph);
if (ret < 0) {
LOGE("Cannot create buffer sink\n");
goto end;
}
ret = av_opt_set_int_list(bufferSinkCtx, "pix_fmts", pix_fmts,
AV_PIX_FMT_YUV420P, AV_OPT_SEARCH_CHILDREN);
if (ret < 0) {
LOGE("Cannot set output pixel format\n");
goto end;
}
/* Endpoints for the filter graph. */
outputs->name = av_strdup("in");
outputs->filter_ctx = bufferSrcCtx;
outputs->pad_idx = 0;
outputs->next = NULL;
inputs->name = av_strdup("out");
inputs->filter_ctx = bufferSinkCtx;
inputs->pad_idx = 0;
inputs->next = NULL;
nb_filters = filter_graph->nb_filters;
if ((ret = avfilter_graph_parse_ptr(filter_graph, filters_descr,
&inputs, &outputs, NULL)) < 0) {
LOGE("avfilter_graph_parse_ptr failed %s %x\n", filters_descr, AVERROR(ret));
goto end;
}
filterCtx = filter_graph->filters[nb_filters];
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0) {
LOGE("avfilter_graph_config failed\n");
goto end;
}
return ret;
end:
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
avfilter_graph_free(&filter_graph);
filter_graph = NULL;
return ret;
}
视频内容绘制文字:
...
// picFrame 存放视频数据的frame,像素格式为yuv420
//添加字幕
int ret = -1;
if (filter_graph && openDrawText) {
ret = av_buffersrc_add_frame_flags(bufferSrcCtx, picFrame, AV_BUFFERSRC_FLAG_PUSH);
if (ret < 0) {
LOGE("Error while feeding the filtergraph ret:%x\n", AVERROR(ret));
}
}
if (ret < 0) {
... // 后续逻辑,如编码,渲染
} else {
index++;
if (filterCtx && index == 200) {
// 修改绘制的文字
int ft = av_opt_set_int(filterCtx->priv, "fontsize", 18, 0 );
LOGE("=========== av_opt_set fontsize ret:%x \n", AVERROR(ft));
av_opt_set(filterCtx->priv, "text", "胜多负少多发发所发一所大所大所大所大所二大所大所大所大所大三所大所大所大所大所四大所大所大\n所大所水五电费水电费水电费水电费水电费水电费", 0 );
}
AVFrame* filterFrame = av_frame_alloc();
/* pull filtered frames from the filtergraph */
while (1) {
ret = av_buffersink_get_frame(bufferSinkCtx, filterFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
if (ret < 0) {
LOGE("Error while get the filtergraph error:%x\n", AVERROR(ret));
break;
}
... // 后续逻辑,如编码,渲染
}
av_frame_free(&filterFrame);
}
上效果图:(图像数据是全0的yuv420数据,所以是绿的)