VAAPI是intel设计的一个视频硬件加速器的软件接口。FFMPEG也将其集成进来。这里通过对源码的分析来了解它的编码流程,尤其是参考帧是如何管理的。
一般情况,编码器的工作周期是一个GOP。GOP通常是封闭的,即下一个GOP不依赖于上一个GOP。这意味着各GOP之间是独立的。在每个GOP内部,每一帧的编码类型(I/P/B)常按照一定的模式来进行。比如,GOP的第一帧一般是I帧,(按编码顺序)第二帧一般是P帧,接着编码B帧。FFMPEG用两个参数来表示一个GOP的长度和模式。第一个参数是GOP size,即一个封闭GOP是由多少帧构成的。另一个参数是B帧的数量,即在两个P帧之间,有多少个B帧。这两个参数,在解码器的上下文中对应的分别是avctx->gope_size和avctx->b_per_p。
编码入口是ff_vaapi_encode2()。输入的原始图像是按顺序进入的,即每次调用编码的avctx->input_order都是递增的。
int ff_vaapi_encode2(AVCodecContext *avctx,
AVPacket *pkt, /* 输出码流 */
const AVFrame *input_image, /*输入原始图像, null at endOfSeq */
int *got_packet) /* 如当前产生码流写1,否则写0 */
{
if (input_image) {
if (input_image->pict_type == AV_PICTURE_TYPE_I) {
err = vaapi_encode_truncate_gop(avctx);
if (err < 0)
goto fail;
ctx->force_idr = 1;
}
/* 获得当前要编码的输入图像。会设置相关的参考帧关系。 */
err = vaapi_encode_get_next(avctx, &pic);
if (err) {
av_log(avctx, AV_LOG_ERROR, "Input setup failed: %d.\n", err);
return err;
}
pic->input_image = av_frame_alloc();
if (!pic->input_image) {
err = AVERROR(ENOMEM);
goto fail;
}
err = av_frame_ref(pic->input_image, input_image);
if (err < 0)
goto fail;
pic->input_surface = (VASurfaceID)(uintptr_t)input_image->data[3];
pic->pts = input_image->pts;
if (ctx->input_order == 0)
ctx->first_pts = pic->pts;
if (ctx->input_order == ctx->decode_delay)
ctx->dts_pts_diff = pic->pts - ctx->first_pts;
if (ctx->output_delay > 0)
ctx->ts_ring[ctx->input_order % (3 * ctx->output_delay)] = pic->pts;
pic->input_available = 1;
}
else
{
if (!ctx->end_of_stream) {
err = vaapi_encode_truncate_gop(avctx);
if (err < 0)
goto fail;
ctx->end_of_stream = 1;
}
}
++ctx->input_order;
++ctx->output_order;
av_assert0(ctx->output_order + ctx->output_delay + 1 == ctx->input_order);
/* 找出下一下编码帧。此帧会在step中完成编码,同时也会用递归的方式先完成其参考帧的编码. */
for (pic = ctx->pic_start; pic; pic = pic->next)
if (pic->encode_order == ctx->output_order)
break;
// pic can be null here if we don't have a specific target in this
// iteration. We might still issue encodes if things can be overlapped,
// even though we don't intend to output anything.
/* 编码当前帧及其参考帧 */
err = vaapi_encode_step(avctx, pic);
if (!pic) {
/* 当前没有有效的输出码流 */
*got_packet = 0;
} else {
/* 输出码流 */
err = vaapi_encode_output(avctx, pic, pkt);
...
*got_packet = 1;
}
/* 清理上下文 */
err = vaapi_encode_clear_old(avctx);
if (err < 0) {
av_log(avctx, AV_LOG_ERROR, "List clearing failed: %d.\n", err);
goto fail;
}
fail:
/* 错误出口,现在没做什么 */
// Unclear what to clean up on failure. There are probably some things we
// could do usefully clean up here, but for now just leave them for uninit()
// to do instead.
return err;
}
当一个GOP结束时,其固定的编码类型模式会受到影响,这时需要一些特殊的处理。因此 代码中处理下一个I帧时要对上一个GOP作截断。
为了管理参考帧,由vaapi_encode_get_next()建立各帧的依赖关系。除了GOP中第一帧编码为I帧,接着会跳过ctx->b_per_p输入帧编一个P帧。然后,将中间跳过的帧全部编码为B帧。这些中间的B帧都以其前面的I帧或P帧为前向参考帧,以其后的P帧为后向参考帧。
static int vaapi_encode_get_next(AVCodecContext *avctx,
VAAPIEncodePicture **pic_out)
{
VAAPIEncodeContext *ctx = avctx->priv_data;
VAAPIEncodePicture *start, *end, *pic;
int i;
for (pic = ctx->pic_start; pic; pic = pic->next) {
if (pic->next)
av_assert0(pic->display_order + 1 == pic->next->display_order);
if (pic->display_order == ctx->input_order) {
*pic_out = pic;
return 0;
}
}
pic = vaapi_encode_alloc();
if (!pic)
return AVERROR(ENOMEM);
if (ctx->input_order == 0 || ctx->force_idr ||
ctx->gop_counter >= avctx->gop_size) {
pic->type = PICTURE_TYPE_IDR;
ctx->force_idr = 0;
ctx->gop_counter = 1;
ctx->p_counter = 0;
} else if (ctx->p_counter >= ctx->p_per_i) {
pic->type = PICTURE_TYPE_I;
++ctx->gop_counter;
ctx->p_counter = 0;
} else {
/* P帧用前一组的最后一帧ctx->pic_end作为参考帧 */
pic->type = PICTURE_TYPE_P;
pic->refs[0] = ctx->pic_end;
pic->nb_refs = 1;
++ctx->gop_counter;
++ctx->p_counter;
}
start = end = pic;
if (pic->type != PICTURE_TYPE_IDR) {
// If that was not an IDR frame, add B-frames display-before and
// encode-after it, but not exceeding the GOP size.
for (i = 0; i < ctx->b_per_p &&
ctx->gop_counter < avctx->gop_size; i++) {
pic = vaapi_encode_alloc();
if (!pic)
goto fail;
/* B帧用当前帧(end)和前一组的最后一帧ctx->pic_end作为参考帧 */
pic->type = PICTURE_TYPE_B;
pic->refs[0] = ctx->pic_end;
pic->refs[1] = end;
pic->nb_refs = 2;
pic->next = start;
pic->display_order = ctx->input_order + ctx->b_per_p - i - 1;
pic->encode_order = pic->display_order + 1;
start = pic;
++ctx->gop_counter;
}
}
if (ctx->input_order == 0) {
pic->display_order = 0;
pic->encode_order = 0;
ctx->pic_start = ctx->pic_end = pic;
} else {
for (i = 0, pic = start; pic; i++, pic = pic->next) {
pic->display_order = ctx->input_order + i;
if (end->type == PICTURE_TYPE_IDR)
pic->encode_order = ctx->input_order + i;
else if (pic == end)
pic->encode_order = ctx->input_order;
else
pic->encode_order = ctx->input_order + i + 1;
}
av_assert0(ctx->pic_end);
ctx->pic_end->next = start;
ctx->pic_end = end;
}
*pic_out = start;
av_log(avctx, AV_LOG_DEBUG, "Pictures:");
for (pic = ctx->pic_start; pic; pic = pic->next) {
av_log(avctx, AV_LOG_DEBUG, " %s (%"PRId64"/%"PRId64")",
picture_type_name[pic->type],
pic->display_order, pic->encode_order);
}
av_log(avctx, AV_LOG_DEBUG, "\n");
return 0;
fail:
while (start) {
pic = start->next;
vaapi_encode_free(avctx, start);
start = pic;
}
return AVERROR(ENOMEM);
}
根据以上分析,当前实现的参考帧管理是比较简单的一种,只是实现了封闭GOP中简单的IPBBB..PBBB..模式。要想实现其它更复杂的模式,只能自己定制相关的实现了。