最近學習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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/240908.html
標籤:其他
