作者: 葉餘 來源: https://www.cnblogs.com/leisure_chn/p/10584937.html
FFmpeg編解碼處理系列筆記:
[0].
FFmpeg時間戳詳解 [1]. FFmpeg編解碼處理1-轉碼全流程簡介 [2]. FFmpeg編解碼處理2-編解碼API詳解 [3]. FFmpeg編解碼處理3-視訊編碼 [4]. FFmpeg編解碼處理4-音頻編碼 基于 FFmpeg 4.1 版本。5. 視訊編碼
編碼使用 avcodec_send_frame() 和 avcodec_receive_packet() 兩個函數。
視訊編碼的步驟:
[1] 初始化打開輸出檔案時建構編碼器上下文
[2] 視訊幀編碼
[2.1] 設定幀類型 "frame->pict_type=AV_PICTURE_TYPE_NONE",讓編碼器根據設定參數自行生成 I/B/P 幀類型
[2.2] 将原始幀送入編碼器,從編碼器取出編碼幀
[2.3] 更新編碼幀流索引
[2.4] 将幀中時間參數按輸出封裝格式的時間基進行轉換
5.1 打開視訊編碼器
完整源碼在 open_output_file() 函數中,下面摘出關鍵部分:
// 3. 建構AVCodecContext
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO ||
dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) // 音頻流或視訊流
{
// 3.1 查找編碼器AVCodec,本例使用與解碼器相同的編碼器
AVCodec *encoder = NULL;
if ((dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) && (strcmp(v_enc_name, "copy") != 0))
{
encoder = avcodec_find_encoder_by_name(v_enc_name);
}
else if ((dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) && (strcmp(a_enc_name, "copy") != 0))
{
encoder = avcodec_find_encoder_by_name(a_enc_name);
}
else
{
encoder = avcodec_find_encoder(dec_ctx->codec_id);
}
if (!encoder)
{
av_log(NULL, AV_LOG_FATAL, "Necessary encoder not found\n");
return AVERROR_INVALIDDATA;
}
// 3.2 AVCodecContext初始化:配置設定結構體,使用AVCodec初始化AVCodecContext相應成員為預設值
AVCodecContext *enc_ctx = avcodec_alloc_context3(encoder);
if (!enc_ctx)
{
av_log(NULL, AV_LOG_FATAL, "Failed to allocate the encoder context\n");
return AVERROR(ENOMEM);
}
// 3.3 AVCodecContext初始化:配置圖像/聲音相關屬性
/* In this example, we transcode to same properties (picture size,
* sample rate etc.). These properties can be changed for output
* streams easily using filters */
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
{
enc_ctx->height = dec_ctx->height; // 圖像高
enc_ctx->width = dec_ctx->width; // 圖像寬
enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio; // 采樣寬高比:像素寬/像素高
/* take first format from list of supported formats */
if (encoder->pix_fmts) // 編碼器支援的像素格式清單
{
enc_ctx->pix_fmt = encoder->pix_fmts[0]; // 編碼器采用所支援的第一種像素格式
}
else
{
enc_ctx->pix_fmt = dec_ctx->pix_fmt; // 編碼器采用解碼器的像素格式
}
/* video time_base can be set to whatever is handy and supported by encoder */
enc_ctx->time_base = av_inv_q(dec_ctx->framerate); // 時基:解碼器幀率取倒數
enc_ctx->framerate = dec_ctx->framerate;
//enc_ctx->bit_rate = dec_ctx->bit_rate;
/* emit one intra frame every ten frames
* check frame pict_type before passing frame
* to encoder, if frame->pict_type is AV_PICTURE_TYPE_I
* then gop_size is ignored and the output of encoder
* will always be I frame irrespective to gop_size
*/
//enc_ctx->gop_size = 10;
//enc_ctx->max_b_frames = 1;
}
else
{
enc_ctx->sample_rate = dec_ctx->sample_rate; // 采樣率
enc_ctx->channel_layout = dec_ctx->channel_layout; // 聲道布局
enc_ctx->channels = av_get_channel_layout_nb_channels(enc_ctx->channel_layout); // 聲道數量
/* take first format from list of supported formats */
enc_ctx->sample_fmt = encoder->sample_fmts[0]; // 編碼器采用所支援的第一種采樣格式
enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate}; // 時基:編碼器采樣率取倒數
// enc_ctx->codec->capabilities |= AV_CODEC_CAP_VARIABLE_FRAME_SIZE; // 隻讀标志
// 初始化一個FIFO用于存儲待編碼的音頻幀,初始化FIFO大小的1個采樣點
// av_audio_fifo_alloc()第二個參數是聲道數,第三個參數是單個聲道的采樣點數
// 采樣格式及聲道數在初始化FIFO時已設定,各處涉及FIFO大小的地方都是用的單個聲道的采樣點數
pp_audio_fifo[i] = av_audio_fifo_alloc(enc_ctx->sample_fmt, enc_ctx->channels, 1);
if (pp_audio_fifo == NULL)
{
av_log(NULL, AV_LOG_ERROR, "Could not allocate FIFO\n");
return AVERROR(ENOMEM);
}
}
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
{
enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 3.4 AVCodecContext初始化:使用AVCodec初始化AVCodecContext,初始化完成
/* Third parameter can be used to pass settings to encoder */
ret = avcodec_open2(enc_ctx, encoder, NULL);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot open video encoder for stream #%u\n", i);
return ret;
}
// 3.5 設定輸出流codecpar
ret = avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder parameters to output stream #%u\n", i);
return ret;
}
// 3.6 儲存輸出流contex
pp_enc_ctx[i] = enc_ctx;
}
5.2 編碼視訊幀
完整源碼在 transcode_video() 函數中,下面摘出關鍵部分:
// 2. 濾鏡處理
ret = filtering_frame(sctx->flt_ctx, frame_dec, frame_flt);
if (ret == AVERROR_EOF)
{
av_log(NULL, AV_LOG_INFO, "filtering vframe EOF\n");
flt_finished = true;
av_frame_free(&frame_flt); // flush encoder
}
else if (ret < 0)
{
av_log(NULL, AV_LOG_INFO, "filtering vframe error %d\n", ret);
goto end;
}
flush_encoder:
// 3. 編碼
if (frame_flt != NULL)
{
// 3.1 設定幀類型。如果不設定,則使用輸入流中的幀類型。
frame_flt->pict_type = AV_PICTURE_TYPE_NONE;
}
// 3.2 編碼
ret = av_encode_frame(sctx->o_codec_ctx, frame_flt, &opacket);
if (ret == AVERROR(EAGAIN)) // 需要讀取新的packet喂給編碼器
{
//av_log(NULL, AV_LOG_INFO, "encode vframe need more packet\n");
goto end;
}
else if (ret == AVERROR_EOF)
{
av_log(NULL, AV_LOG_INFO, "encode vframe EOF\n");
enc_finished = true;
goto end;
}
else if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "encode vframe error %d\n", ret);
goto end;
}
// 3.3 更新編碼幀中流序号,并進行時間基轉換
// AVPacket.pts和AVPacket.dts的機關是AVStream.time_base,不同的封裝格式其AVStream.time_base不同
// 是以輸出檔案中,每個packet需要根據輸出封裝格式重新計算pts和dts
opacket.stream_index = sctx->stream_idx;
av_packet_rescale_ts(&opacket, sctx->o_codec_ctx->time_base, sctx->o_stream->time_base);
// 4. 将編碼後的packet寫入輸出媒體檔案
ret = av_interleaved_write_frame(sctx->o_fmt_ctx, &opacket);
av_packet_unref(&opacket);
5.3 視訊編碼中的 I/B/P 幀類型
做一個實驗,修改 5.1.2 節 frame_flt->pict_type 值和 5.1.1 節 enc_ctx->gop_size 和 enc_ctx->max_b_frames,将編碼後視訊幀 I/B/P 類型列印出來,觀察實驗結果。
我們選一個很短的視訊檔案用于測試(右鍵另存為):
tnmil3.flvtnmil3.flv 重命名為 tnmil.flv
轉碼:tnmil.flv ==> tnmilo1.flv
指令:./transcode -i tnmil.flv -c:v copy -c:a copy tnmilo1.flv
tnmil.flv ==> tnmilo1.flv 不修改frame IBP類型,不設定編碼器gop_size和max_b_frames
IBPBPBPBPBPBBBPBBBPBBBPBBPBBBPBBBPBBPBBBPBBBPBBBPBPBPBBPBBBPBBBPBPBBBPBBBPBPBBPBBBPPIBBP
IBPBPBPBPBPBBBPBBBPBBBPBBPBBBPBBBPBBPBBBPBBBPBBBPBPBPBBPBBBPBBBPBPBBBPBBBPBPBBPBBBPPIBBP
tnmil.flv ==> tnmilo3.flv 将frame IBP類型設為NONE,将編碼器gop_size設為10,max_b_frames設為1
IBPBPBPBPBPBBBPBBBPBBBPBBPBBBPBBBPBBPBBBPBBBPBBBPBPBPBBPBBBPBBBPBPBBBPBBBPBPBBPBBBPPIBBP
IBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPPIBPP
tnmilo3.flv ==> tnmilo4.flv 不修改frame IBP類型,不設定編碼器gop_size和max_b_frames
IBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPPIBPP
IBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPPIBPP
tnmilo3.flv ==> tnmilo5.flv 将frame IBP類型設為NONE,不設定編碼器gop_size(預設-1)和max_b_frames(預設0)
IBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPBPBPBPPIBPPIBPP
IBPBPBPBPBBBPBPBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBBBPBPBBBPPIBBP
實驗結論如下:将原始視訊幀 frame 送入視訊編碼器後生成編碼幀 packet,那麼
[1] 手工設定每一幀 frame 的幀類型為 I/B/P,則編碼後的 packet 的幀類型和 frame 中的一樣。編碼器是否設定 gop_size 和 max_b_frames 兩個參數無影響。
[2] 将每一幀 frame 的幀類型設定為 NONE,如果未設定編碼器的 gop_size(預設值 -1)和 max_b_frames (預設值 0)兩個參數,則編碼器自動選擇合适參數來進行編碼,生成幀類型。
[3] 将每一幀 frame 的幀類型設定為 NONE,如果設定了編碼器的 gop_size 和 max_b_frames 兩個參數,則編碼器按照這兩個參數來進行編碼,生成幀類型。
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
