背景
时间:2024.09.25
项目中需要给播放的视频添加水印,工程本身有集成 ijkplayer 所以想利用 ffmpeg 来操作添加水印。默认 ffmpeg 配置中是不包含 h264编码器的,需要手动给 ffmpeg 添加h264编码器,如何集成h264编码器,可以看我这篇文章。
在网上找了很多资料,大部分是通过命令行操作的,通过 ffmpeg 的sdk编码方式来添加水印的文章不是很多,但也找到了几篇文章,是以代码方式添加水印的,文章以 qt 或者 c++语言的居多,将代码拷贝到自己的工程里面,修修改改也能跑起来,但是效果不是很好,很多细节没有处理到位。
所以就想着自己写一个添加水印的 demo,ffmpeg 我也是才学习不久,算是刚刚摸到门把手的小白,可能我写的代码里面也有很多不完善的地方,如果有看到哪些错误的地方,希望能给我留言或者添加我的 vx: isMeJson 进行指正。
工程环境
ijkplayer 的 ffmpeg 的版本是 3.4 ,我将 3.4 版本的源码下载到本地进行编译,发现编译不过,所以就用了 4.3.2 的版本。编译 ffmpeg4.3.2 版本参考小码哥的这篇文章,我编译的时候没有任何报错。
通过编译命令,将编译好的 lib 库,放在了 /usr/local/ffmpeg4_32
目录下,编译的时候可能会有权限报错,建议安装到/opt/local/ffmpeg4_32
,ffmpeg4_32 是我定义的文件名,为了跟其他版本的 ffmpeg 进行区分。
./configure --prefix=/usr/local/ffmpeg4_32 --enable-shared --disable-static --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265
这是我编译好的配置信息
ffmpeg version 4.3.2 Copyright (c) 2000-2021 the FFmpeg developers
built with Apple clang version 15.0.0 (clang-1500.3.9.4)
configuration: --prefix=/usr/local/ffmpeg4_32 --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265 --enable-filter=delogo --disable-optimizations --enable-libspeex --enable-videotoolbox --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --cc=clang --host-cflags=
libavutil 56. 51.100 / 56. 51.100
libavcodec 58. 91.100 / 58. 91.100
libavformat 58. 45.100 / 58. 45.100
libavdevice 58. 10.100 / 58. 10.100
libavfilter 7. 85.100 / 7. 85.100
libswscale 5. 7.100 / 5. 7.100
libswresample 3. 7.100 / 3. 7.100
libpostproc 55. 7.100 / 55. 7.100
编译好大概这样。
我是用 Xcode 创建的 c++环境,用 Xcode 创建了一个命令行工程。
然后将 ffmpeg 编译好的 lib
和include
拷贝到了工程目录下。然后配置.a静态库路径 Libary Search Paths
和ffmpeg的头文件 User Header Search Paths
。 同时将.a静态库拷贝到 Frameworks and Libraries
下。我是将 .a 和 .dylib
一股脑的拷贝到工程下,只有.a貌似会报错,但是手机上的 ffmpeg 是没有报错的。
添加水印思路
- 输入文件解封装,创建文件上下文,找到视频流,并初始化视频解码器。
- 初始化输出数据编码上下文,通过找到的视频流,拷贝参数到编码器。
- 初始化输出文件格式上下文,我输出的是 ts 文件,所以用的
mpegts
,并与输出文件路径进行 IO 绑定。 - 再初始化 filter过滤器,里面有两个 filter,一个是
buffer
,buffersink
,我的理解filter就像一个管道,将处理的数据从一个管道流向下一个管道。过滤器这块了解的不多,是将网上的代码给摘抄拼合的。 - 处理视频流,将视频流解封装,然后通过过滤器添加水印,最终将视频原始数据进行 h264 编码。
- 最后冲刷解码器和编码器(这一步很重要,如果不做会缺失数据)。
- 释放创建的对象。
大概就是这么几步,或许画个图会更直观一些。
实现代码
如果不想看部分代码,可以点击 代码文件下载 进行整体代码浏览。
- 输入文件上下文创建
static int open_input_file(const char *filename) {
int ret = -1;
AVCodec *dec = nullptr;
// 1.文件解封装,打开文件上下文
ret = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
if (ret < 0) {
av_log(nullptr, AV_LOG_ERROR, "avformat_open_input error");
return ret;
}
//2.找到视频流
ret = avformat_find_stream_info(fmt_ctx, nullptr);
if (ret < 0) {
av_log(nullptr, AV_LOG_ERROR, "avformat_find_stream_info error");
return ret;
}
// 3.找到视频流,并获取解码器
ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0);
if (ret < 0) {
av_log(nullptr, AV_LOG_ERROR, "av_find_best_stream error----> 没有视频流");
return ret;
}
// 获取到视频的下标
v_stream_index = ret;
// 4.找到流之后,创建解码上下文,并打开解码器
// 解码上下文
dec_ctx = avcodec_alloc_context3(dec);
if (!dec_ctx) {
// 内存不足,创建失败.
return AVERROR(ENOMEM);
}
// 获取上下文,拷贝 parameter 信息
ret = avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[v_stream_index]->codecpar);
if (ret) {
av_log(nullptr, AV_LOG_ERROR, "avcodec_parameters_to_context error");
return ret;
}
// 打开解码器
ret = avcodec_open2(dec_ctx, dec, nullptr);
if (ret) {
av_log(nullptr, AV_LOG_ERROR, "avcodec_open2 error");
return ret;
}
return 0;
}
- 输出数据上下文创建
static int init_outfmt_context(const char *filename) {
// 输出格式上下文,绑定视频流和音频流等
// 输出为 ts 文件数据
int ret = avformat_alloc_output_context2(&out_fmt_ctx, nullptr, "mpegts", filename);
if (ret < 0) {
FFM_AV_LOG("avformat_alloc_output_context2 --------->out_fmt_ctx error");
return ret;
}
// 初始化 stream_map.
// 有几条流就初始化几条
stream_map = (int *)av_calloc(fmt_ctx->nb_streams, sizeof(int));
int stream_idx = 0;
for (int i = 0 ; i < fmt_ctx->nb_streams; i++) {
// 取出每条流,判断流的类型,进行过滤
AVStream *inStream = fmt_ctx->streams[i];
AVCodecParameters *inCodecPar = inStream->codecpar;
if (inCodecPar->codec_type != AVMEDIA_TYPE_VIDEO
&& inCodecPar->codec_type != AVMEDIA_TYPE_AUDIO
&& inCodecPar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
// 不是视频\音频\字幕流都过滤掉
stream_map[i] = -1;
continue;
}
stream_map[i] = stream_idx++;
// 创建新的流
AVStream *outStream = avformat_new_stream(out_fmt_ctx, nullptr);
if (!outStream) {
FFM_AV_LOG("avformat_new_stream --------->error");
return -1;
}
// 拷贝流参数
avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
outStream->codecpar->codec_tag = 0;
}
//打开输出IO 绑定上下文
ret = avio_open(&out_fmt_ctx->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0) {
FFM_AV_LOG("avio_open ---------> error");
return ret;
}
//写多媒体文件头到目标的文件
ret = avformat_write_header(out_fmt_ctx, NULL);
if (ret < 0) {
FFM_AV_LOG("avformat_write_header ----> error");
return ret;
}
return 0;
}
- 初始化过滤器
static int init_filters(const char *filter_desc) {
int ret = -1;
AVFilterInOut *inputs = avfilter_inout_alloc();
AVFilterInOut *outputs = avfilter_inout_alloc();
if (!inputs || !outputs) {
FFM_AV_LOG("avfilter_inout_alloc error");
return AVERROR(ENOMEM);
}
graph = avfilter_graph_alloc();
if (!graph) {
av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_alloc error");
return AVERROR(ENOMEM);
}
// 输入 buffer
const AVFilter *bufsrc = avfilter_get_by_name("buffer");
if (!bufsrc) {
av_log(nullptr, AV_LOG_ERROR, "avfilter_get_by_name error --->buffer");
return -1;
}
// 输出 buffersink
const AVFilter *bufsink = avfilter_get_by_name("buffersink");
if (!bufsink) {
av_log(nullptr, AV_LOG_ERROR, "avfilter_get_by_name error --->bufsink");
return -1;
}
enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE};
char args[512] = {};
AVRational time_base = fmt_ctx->streams[v_stream_index]->time_base;
snprintf(args, 512, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",dec_ctx->width,
dec_ctx->height, dec_ctx->pix_fmt, time_base.num, time_base.den, dec_ctx->sample_aspect_ratio.num, dec_ctx->sample_aspect_ratio.den);
// 创建输入 filter
ret = avfilter_graph_create_filter(&buf_ctx, bufsrc, "in", args, nullptr, graph);
if (ret) {
av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_create_filter error----->buf_ctx");
goto __ERROR;
}
// 创建输出 filter
ret = avfilter_graph_create_filter(&bufsink_ctx, bufsink, "out", nullptr, nullptr, graph);
if (ret) {
av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_create_filter error------>bufsink_ctx");
goto __ERROR;
}
av_opt_set_int_list(bufsink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
inputs->name = av_strdup("out");
inputs->filter_ctx = bufsink_ctx;
inputs->pad_idx = 0;
inputs->next = nullptr;
outputs->name = av_strdup("in");
outputs->filter_ctx = buf_ctx;
outputs->pad_idx = 0;
outputs->next = nullptr;
// 创建 filter 并且添加到graph 描述符
ret = avfilter_graph_parse_ptr(graph, filter_desc, &inputs, &outputs, nullptr);
if (ret < 0) {
av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_parse_ptr error");
goto __ERROR;
}
ret = avfilter_graph_config(graph, nullptr);
if (ret < 0) {
FFM_AV_LOG("avfilter_graph_config ERROR")
goto __ERROR;
}
return 0;
__ERROR:
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return ret;
}
- 对视频流做解码、添加水印、编码操作
// 获取解码后的原始数据 PCM/YUV
// 将数据添加到 buffer filter 中
// 从 buffer sink 中获取处理好的数据
AVPacket packet;
AVFrame *frame = nullptr;
AVFrame *filt_frame = nullptr;
frame = av_frame_alloc();
filt_frame = av_frame_alloc();
if (!frame || !filt_frame) {
FFM_AV_LOG("av_frame_alloc----------->ERROR");
return -1;
}
int video_index = 0;
// 读取数据
while (true) {
ret = av_read_frame(fmt_ctx, &packet);
if (ret == AVERROR_EOF) {
// 读到文件尾部
break;
}else if (ret == 0) {
if (stream_map[packet.stream_index] < 0) {
// 不是需要的数据,丢弃
av_packet_unref(&packet);
continue;
}
printf("mhy输入-------------------------------------------%lld\n", packet.dts);
// 修改 packet 的 index 为输出上下文中的顺序
int orgStreamIndex = packet.stream_index;
// 改为输出文件上下文中的流下标
packet.stream_index = stream_map[packet.stream_index];
AVStream *inStream, *outStream;
inStream = fmt_ctx->streams[orgStreamIndex];
//此时 out 的stream_index已经改为了输出上下文中的顺序
outStream = out_fmt_ctx->streams[packet.stream_index];
// 重新设置流的 pts dts duration
av_packet_rescale_ts(&packet, inStream->time_base, outStream->time_base);
packet.pos = -1;
if (orgStreamIndex == v_stream_index) {
// 视频流
// 将 pkt 数据送到解码器解码
// printf("mhy输入----------------------%d \n", video_index++);
ret = avcodec_send_packet(dec_ctx, &packet);
if (ret < 0) {
FFM_AV_LOG("avcodec_send_packet ----pkt error");
goto __ERROR;
}
ret = decode_frame_and_filter(frame, filt_frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
// 读完,或没有数据了,再读取
continue;
}
if (ret < 0) {
FFM_AV_LOG("decode_frame_and_filter ---------- error");
goto __ERROR;
}
}else {
// 其他流,不做处理,直接写入本地
printf("mhy输出转换1-------------------------------------------%lld\n", packet.dts);
av_interleaved_write_frame(out_fmt_ctx, &packet);
}
av_packet_unref(&packet);
}else {
continue;
}
}
// 刷新解码缓冲区
avcodec_send_packet(dec_ctx, nullptr);
decode_frame_and_filter(frame, filt_frame);
// 刷新编码缓冲区
do_frame(nullptr);
//添加数据结束字符
ret = av_write_trailer(out_fmt_ctx);