最近学习FFmpeg编程开发,想写个视频添加水印图片的demo(未对音频或字幕进行处理),代码编写中遇见很多问题,在这里进行做一个笔记来,易于自己记忆和理解。期间在网上找demo,发现很多都是ffmpeg3版本的一些demo,ffmpeg4有很大的改变,有很多方法不适用,因此写篇文章给初学者一些细微的帮助,也易于自己巩固,避免犯类似的错误。
一、总结一下编码过程:
1.初始化化封装格式上下文 并打开文件
avformat_alloc_context / avformat_alloc_output_context2
2.创建输出码流
avformat_new_stream
3.创建编码器(根据输入编码类型)
avcodec_find_encoder
4.创建视频编码器上下文 并设置参数
avcodec_alloc_context3
5.打开编码器
avcodec_open2
6.把编码器信息写入流中
avcodec_parameters_from_context
7.创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
avio_open
8.写入头文件信息
avformat_write_header
9.初始化过滤器 并过滤
init_filter av_buffersrc_add_frame_flags av_frame_make_writable
10.将编码后的视频压缩数据写入文件中
①设置帧类型②发送过滤的frame,接收packet③更新编码帧中流序号,并进行时间基转换④压缩数据写入文件中
11.写入文件尾部信息
av_write_trailer
12.释放资源
二、代码片段
1.输入文件的解码以及编码
int FfmpegTest::open_input_file(const char *filename)
{
int ret;
// 1. 打开视频文件:读取文件头,将文件格式信息存储在ifmt_ctx中
if ((ret = avformat_open_input(&ifmt_ctx, filename, NULL, NULL)) < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
return ret;
}
// 2. 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入ifmt_ctx.streams
// ifmt_ctx.streams是一个指针数组,数组大小是ifmt_ctx.nb_streams
if ((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
return ret;
}
// 每路音频流/视频流一个AVCodecContext
ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (ret < 0)
{
cout << "no find best stream";
return -1;
}
videoindex = ret;
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
av_log(NULL, AV_LOG_ERROR, "Failed to allocate the decoder context for stream #%u\n");
return AVERROR(ENOMEM);
}
// 3.3 AVCodecContext初始化:使用codec参数codecpar初始化AVCodecContext相应成员
ret = avcodec_parameters_to_context(codec_ctx, ifmt_ctx->streams[videoindex]->codecpar);
if (ret < 0)
{
return ret;
}
ret = avcodec_open2(codec_ctx, codec, NULL);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to open decoder for stream #%u\n", ret);
return ret;
}
av_dump_format(ifmt_ctx, 0, filename, 0);
if (!init_filter(codec_ctx))
return -1;
if (open_output_file("uuu.mp4") < 0)
return -1;
AVPacket *packet;
packet = (AVPacket*)av_malloc(sizeof(AVPacket));
AVPacket *outpacket=nullptr;
outpacket = (AVPacket*)av_malloc(sizeof(AVPacket));
AVFrame *frame;
frame = av_frame_alloc();
AVFrame *outframe;
outframe = av_frame_alloc();
bool write_status = true;
AVBufferRef *bufferRef;
while (av_read_frame(ifmt_ctx, packet) >= 0)
{
//时间基转换,解码
av_packet_rescale_ts(packet, ifmt_ctx->streams[videoindex]->time_base, encode_ctx->time_base);
if (packet->stream_index == videoindex)
{
ret = avcodec_send_packet(codec_ctx, packet);
if (ret < 0)
{
cout << "avcodec_send_packet" << endl;
break;
}
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EINVAL))
{
cout << "codec not opened, or it is an encoder" << endl;
goto end;
}
else if (ret == AVERROR(EAGAIN))
{
cout << "user must try to send new input" << endl;
continue;
}
else if (ret == AVERROR_EOF)
{
cout << "the decoder has been fully flushed" << endl;
break;
}
else if (ret >= 0)
{
frame->pts = frame->best_effort_timestamp;
//push the decoded frame into the filtergrapth
if (av_buffersrc_add_frame_flags(bufsrc_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0)
{
cout << "Error while feeding the filtergraph" << endl;
break;
}
//pull filtered frames from the filtergraph
while (1)
{
ret = av_buffersink_get_frame(bufsink_ctx, outframe);
if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
break;
if (ret < 0)
goto end;
if (!outframe || !encode_ctx)
break;
if (av_frame_make_writable(outframe) < 0)
{
cout << "av_frame_make_writable NO!" << endl;
break;
}
// 设置帧类型
outframe->pict_type = AV_PICTURE_TYPE_NONE;
//10.将编码后的视频压缩数据写入文件中
while (1)
{
write_status = true;
ret = avcodec_send_frame(encode_ctx, outframe);
if (ret < 0)
{
cout << "avcodec_send_frame failed!" << endl;
goto end;
}
ret = avcodec_receive_packet(encode_ctx, outpacket);
if (ret == AVERROR_EOF || ret == AVERROR(EINVAL))
{
break;
}
else if(ret == AVERROR(EAGAIN))
{
continue;
}
else if (ret >= 0)
{
// 3.3 更新编码帧中流序号,并进行时间基转换
// AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式其AVStream.time_base不同
// 所以输出文件中,每个packet需要根据输出封装格式重新计算pts和dts
outpacket->stream_index = videoindex;
AVRational dst = { 1,25 };
av_packet_rescale_ts(outpacket, encode_ctx->time_base, ofm_ctx->streams[0]->time_base);
//将编码后的packet写入输出媒体文件
ret = av_interleaved_write_frame(ofm_ctx, outpacket);
av_packet_unref(outpacket);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "write vframe error %d\n", ret);
}
else
cout << "av_interleaved_write_frame success" << endl;
write_status = false;
break;
}
}
av_frame_unref(outframe);
if (!write_status)
break;
}
}
}
}
av_packet_unref(packet);
}
//11.写入文件尾部信息
ret = av_write_trailer(ofm_ctx);
cout << "success" << endl;
end:
avfilter_graph_free(&filter_graph);
av_packet_unref(packet);
av_packet_unref(outpacket);
av_frame_unref(outframe);
av_frame_unref(frame);
avcodec_close(codec_ctx);
avformat_close_input(&ifmt_ctx);
avcodec_free_context(&encode_ctx);
avformat_free_context(ofm_ctx);
return -1;
}
2.初始化过滤器
bool FfmpegTest::init_filter(AVCodecContext *codec_ctx)
{
char args[512] = "";
int ret = -1;
AVFilter *bufsink = (AVFilter*)avfilter_get_by_name("buffersink");
AVFilter *bufsrc = (AVFilter*)avfilter_get_by_name("buffer");
AVFilterInOut *input = avfilter_inout_alloc();
AVFilterInOut *output = avfilter_inout_alloc();
AVPixelFormat fmt1[] = { (AVPixelFormat)codec_ctx->pix_fmt, AV_PIX_FMT_NONE};
AVRational time_base = ifmt_ctx->streams[videoindex]->time_base;
filter_graph = avfilter_graph_alloc();
snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->time_base.num,
codec_ctx->time_base.den, codec_ctx->sample_aspect_ratio.num, codec_ctx->sample_aspect_ratio.den);
ret = avfilter_graph_create_filter(&bufsrc_ctx, bufsrc, "in", args, NULL, filter_graph);
if (ret < 0)
{
cout << "bufsrc_ctx avfilter_graph_create_filter failed" << endl;
return false;
}
ret = avfilter_graph_create_filter(&bufsink_ctx, bufsink, "out", NULL, NULL, filter_graph);
if (ret < 0)
{
cout << "avfilter_graph_create_filter failed" << endl;
return false;
}
ret = av_opt_set_int_list(bufsink_ctx, "pix_fmts", fmt1, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
if (ret < 0)
{
cout << "Cannot set output pixel format" << endl;
return false;
}
output->name = av_strdup("in");
output->filter_ctx = bufsrc_ctx;
output->pad_idx = 0;
output->next = nullptr;
input->name = av_strdup("out");
input->filter_ctx = bufsink_ctx;
input->pad_idx = 0;
input->next = nullptr;
const char *filter_desc = "movie=Mario.png[wm];[in][wm]overlay=0:0[out]";//添加水印到坐标5,5 (这儿一定注意是否超出范围坐标)
//const char *filter_desc = "scale=78:24,transpose=cclock";
//const char *filter_desc = "drawtext = fontfile = simhei.ttf: text = 'we are family' : x = 10 : y = 10 : fontsize = 24 : fontcolor = white : shadowy = 2";
ret = avfilter_graph_parse(filter_graph, filter_desc, input, output, NULL);
if (ret < 0)
{
return false;
}
ret = avfilter_graph_config(filter_graph, NULL);
if (ret < 0)
return false;
return true;
}
3.输出文件的初始化
int FfmpegTest::open_output_file(const char *filename)
{
ret = -1;
//1.初始化化封装格式上下文 并打开文件
ofm_ctx = avformat_alloc_context();
ret = avformat_alloc_output_context2(&ofm_ctx, NULL, NULL, filename);
if (ret < 0)
{
cout << "avformat_alloc_output_context2 failed!" << endl;
goto end;
}
//2.创建输出码流
a_stream = avformat_new_stream(ofm_ctx, NULL);
//3.创建编码器(根据输入编码类型)
encode = avcodec_find_encoder(ifmt_ctx->streams[videoindex]->codecpar->codec_id);
//4.视频编码器上下文 并设置参数
encode_ctx = avcodec_alloc_context3(encode);
encode_ctx->codec_id = codec_ctx->codec_id;
encode_ctx->codec_type = codec_ctx->codec_type;
//encode_ctx->frame_size = codec_ctx->frame_size;
encode_ctx->height = codec_ctx->height;
encode_ctx->width = codec_ctx->width;
encode_ctx->sample_aspect_ratio = codec_ctx->sample_aspect_ratio; // 采样宽高比:像素宽/像素高
encode_ctx->refs = codec_ctx->refs;
encode_ctx->extradata = codec_ctx->extradata;
encode_ctx->delay = codec_ctx->delay;
encode_ctx->framerate = codec_ctx->framerate;
encode_ctx->time_base.den = 25;
encode_ctx->time_base = av_inv_q(codec_ctx->framerate); // 时基:解码器帧率取倒数
encode_ctx->time_base.num = 1;
encode_ctx->pix_fmt = codec_ctx->pix_fmt; // 编码器采用解码器的像素格式
encode_ctx->bit_rate = codec_ctx->bit_rate;//设置自己的比特率
encode_ctx->max_b_frames = 0;
//5.打开编码器
ret = avcodec_open2(encode_ctx, encode, NULL);
if (ret < 0)
{
cout << "avcodec open failed!" << endl;
goto end;
}
//6.把编码器信息写入流中
ret = avcodec_parameters_from_context(a_stream->codecpar, encode_ctx);
if (ret < 0)
{
cout << "avcodec_parameters_from_context failed!" << endl;
goto end;
}
av_dump_format(ofm_ctx, 0, filename, 1);
if (!(ofm_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究AVFMT_NOFILE标志
{
// 7. 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
ret = avio_open(&ofm_ctx->pb, filename, AVIO_FLAG_READ_WRITE);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
goto end;
}
}
//8.写入头文件信息(前提得初始化一个AVIOContext)
ret = avformat_write_header(ofm_ctx, NULL);
if (ret < 0)
{
cout << "write header failed!" << endl;
goto end;
}
return 0;
end:
avcodec_free_context(&encode_ctx);
avformat_free_context(ofm_ctx);
return -1;
}
整体上的代码函数就这三个,没有进行细化函数,存在许多优化,本身就是一个demo,能实现功能即可。
三、遇到问题
因之前一开始对编码和解码都是属于混乱状态,一直没理解,出现很多类似的问题,找问题出处,很难下手都是一一尝试,最后才解决的。
1.运行到avformat_write_header()时,ffmpeg_transcode.exe 中)引发的异常: 0xC0000005: 读取位置 0x0000000000000088 时发生访问冲突
write之前并没有执行avio_open(),所以一直报异常
if (!(ofm_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究AVFMT_NOFILE标志
{
// 7. 创建并初始化一个AVIOContext,用以访问URL(out_filename)指定的资源
ret = avio_open(&ofm_ctx->pb, filename, AVIO_FLAG_READ_WRITE);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open output file '%s'", filename);
goto end;
}
}
2.过滤器初始化过程中错误,导致后面进行avcodec_send_frame或者avcodec_receive_packet也报类似的异常
①原因是格式设置错误,不对应,fmt格式应该于输入格式的pix_fmt对应。
AVPixelFormat fmt1[] = { (AVPixelFormat)codec_ctx->pix_fmt, AV_PIX_FMT_NONE};
②添加水印图片时,坐标起点加上图标大小超出界限
const char *filter_desc = "movie=Mario.png[wm];[in][wm]overlay=0:0[out]";//添加水印到坐标0.0
3.编码后的视频出现时长不对,可能导致时间缩小(快进几倍的样子),时间变长(扩大播放时间)
原因是没有进行时间基转换av_packet_rescale_ts,得设置对应的时间基,不然就会出现问题。
解码时需要进行时间基转换,编码也需要进行转换,才能对应 (花了长时间才发现的 )
可参考:https://www.cnblogs.com/leisure_chn/p/10584910.html
以上问题就是在编写过程中,花了时间去解决的问题,很多都能避免的。学习新东西,就应该脚踏实地的去实践,不要想着直接把别人的demo进行复制,实现即可。还是需要自己理清思路一步一步实现,解决其中问题,才能更好掌握知识。
学习方法:先掌握基础知识和接口函数调用,再去看逻辑思路,最后再看原理和算法(借用我领导给我讲的东西,总结了一下)
时刻提醒自己,一步一步脚印来,不要好高骛远。
参考:http://ffmpeg.org/doxygen/trunk/examples.html