下面是解码的过程代码,对输入给解码器的pkt桢类型进行判断,关键桢打印出is key frame
,解码出来的桢根据pict_type打印桢类型出I/P/B桢类型,从这里也可以看出来,没解码之前,AVPacket只能得到是否关键帧,要知道桢类型,必须在解码后。
完整代码可以从github上获取
c
/* Read packets from input file and decode them */
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream_idx) {
if (pkt->flags & AV_PKT_FLAG_KEY) {
av_log(NULL, AV_LOG_INFO, " in is key frame!\n");
} else {
av_log(NULL, AV_LOG_INFO, " in is't key frame!\n");
}
/* Send packet to decoder */
ret = avcodec_send_packet(codec_ctx, pkt);
if (ret < 0) {
fprintf(stderr, "Error sending packet to decoder\n");
exit(1);
}
/* Receive frame from decoder */
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
fprintf(stderr, "Error receiving frame from decoder\n");
exit(1);
}
if (frame->pict_type == AV_PICTURE_TYPE_I) {
av_log(NULL, AV_LOG_INFO, " out is I frame!\n");
} else if (frame->pict_type == AV_PICTURE_TYPE_P) {
av_log(NULL, AV_LOG_INFO, " out is P frame!\n");
} else if (frame->pict_type == AV_PICTURE_TYPE_B) {
av_log(NULL, AV_LOG_INFO, " out is B frame!\n");
}
/* Write YUV data to output file */
fwrite(frame->data[0], 1, codec_ctx->width * codec_ctx->height, outfile);
fwrite(frame->data[1], 1, codec_ctx->width * codec_ctx->height / 4, outfile);
fwrite(frame->data[2], 1, codec_ctx->width * codec_ctx->height / 4, outfile);
}
}
av_packet_unref(pkt);
}
FFmpeg解码流程
avcodec_send_packet
先对avpkt进行ref操作,然后发送给bsf,然后判断avci->buffer_frame->buf
为NULL,就调用decode_receive_frame_internal
进行解码,进入decode_simple_internal
后,ff_decode_get_packet
会先从bsf中获取packet,然后调用解码器解码函数进行解码。
avcodec_receive_frame
中也会先判断avci->buffer_frame->buf[0]
,如果不为NULL,说明前面send_packet的时候已经解码出来了,这次调用只需要进行ref操作。如果avci->buffer_frame->buf[0]
为NULL,调用decode_receive_frame_internal解码,和前面send_packet中的调用流程一样。
bash
avcodec_send_packet
-> av_packet_ref(avci->buffer_pkt, avpkt)
-> av_bsf_send_packet(avci->bsf, avci->buffer_pkt)
-> !avci->buffer_frame->buf[0]
-> decode_receive_frame_internal
-> decode_simple_receive_frame(avctx, frame)
-> decode_simple_internal
-> ff_decode_get_packet
-> av_bsf_receive_packet
-> ff_bsf(ctx->filter)->filter(ctx, pkt)
-> h264_decode_frame
got_frame -> return
avcodec_receive_frame # 循环receive,直到返回EAGAIN
-> avci->buffer_frame->buf[0] # 前面已经解码出来
-> av_frame_move_ref(frame, avci->buffer_frame) #返回继续解码
-> !avci->buffer_frame->buf[0] #前面没有解码
-> decode_receive_frame_internal
-> decode_simple_receive_frame(avctx, frame)
-> decode_simple_internal
-> ff_decode_get_packet
-> av_bsf_receive_packet
-> ff_bsf(ctx->filter)->filter(ctx, pkt)
-> h264_decode_frame
got_frame -> return
解码含B桢的视频
解码含B桢的视频,通过ffprobe把前面几桢的frame信息输出到xml,可以看到桢的分布如下:
bash
ffprobe -show_frames -select_streams v -of xml ~/video/b-frame.mp4 > videoframes.info
xml
<frame key_frame="1" pts="0" pkt_dts="0" pkt_dts_time="0.000000" width="1920" height="1080" pix_fmt="yuv420p" pict_type="I">
<side_data_list>
<side_data type="H.26[45] User Data Unregistered SEI message">
<side_datum key="side_data_type" value="H.26[45] User Data Unregistered SEI message"/>
</side_data>
</side_data_list>
</frame>
<frame key_frame="0" pkt_dts="512" pkt_dts_time="0.040000" pkt_size="921" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="1024" pkt_dts_time="0.080000" pkt_size="250" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="1536" pkt_dts_time="0.120000" pkt_size="2663" width="1920" height="1080" pix_fmt="yuv420p" pict_type="P" />
<frame key_frame="0" pkt_dts="2048" pkt_dts_time="0.160000" pkt_size="400" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="2560" pkt_dts_time="0.200000" pkt_size="247" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="3072" pkt_dts_time="0.240000" pkt_size="774" width="1920" height="1080" pix_fmt="yuv420p" pict_type="P" />
<frame key_frame="0" pkt_dts="3584" pkt_dts_time="0.280000" pkt_size="353" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="4096" pkt_dts_time="0.320000" pkt_size="197" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="4608" pkt_dts_time="0.360000" pkt_size="206" width="1920" height="1080" pix_fmt="yuv420p" pict_type="P" />
<frame key_frame="0" pkt_dts="5120" pkt_dts_time="0.400000" pkt_size="332" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="5632" pkt_dts_time="0.440000" pkt_size="15012" width="1920" height="1080" pix_fmt="yuv420p" pict_type="B" />
<frame key_frame="0" pkt_dts="6144" pkt_dts_time="0.480000" pkt_size="20861" width="1920" height="1080" pix_fmt="yuv420p" pict_type="P" />
从前面的xml中可以看到输入桢的次序是:
bash
I B B P B B P B B P
cs
in is key frame! # in I
in is't key frame! # in B
in is't key frame! # in B
# 进三桢I B B解码出一帧I
out is I frame! # out I
# 然后进一帧P解码出一帧B
in is't key frame! # in P
out is B frame! # out B
# 然后进一帧B解码出一帧B
in is't key frame! # in B
out is B frame! # out B
# 然后进一帧B解码出一帧P
in is't key frame! # in B
out is P frame! # out P
# 然后进一帧P解码出一帧B
in is't key frame! # in P
out is B frame! # out B
# 然后进一帧B解码出一帧B
in is't key frame! # in B
out is B frame! # out B
解码不含B桢的视频
通过debug和log可以看出,不含B桢的视频是进一帧出一帧,avcodec_send_packet
之后,decoder实际上会被调用一次解码出来一帧,avcodec_receive_frame
中调用ff_decode_receive_frame
也是判断avci->buffer_frame->buf[0]
是否为NULL,不为NULL时,av_frame_move_ref
返回,不会再走decode_receive_frame_internal
解码流程。
bash
in is key frame!
out is I frame!
in is key frame!
out is I frame!
in is key frame!
out is I frame!
in is key frame!
所以使用avcodec_send_packet
/avcodec_receive_frame
,avcodec_send_packet
之后需要将frame及时取出来,不然第三次avcodec_send_packet
之后,判断avci->buffer_pkt
的地方就会返回EAGAIN。
c
if (!AVPACKET_IS_EMPTY(avci->buffer_pkt))
return AVERROR(EAGAIN);
原因是这样的:
第一个avcodec_send_packet
进来,av_packet_ref(avci->buffer_pkt, avpkt)
后,调用decode_receive_frame_internal
解码,正常返回。
第二个avcodec_send_packet
进来,av_packet_ref(avci->buffer_pkt, avpkt)
后,因为avci->buffer_frame->buf[0]
不为NULL,没有调用decode_receive_frame_internal
解码消耗掉。
cif (consumed >= pkt->size || ret < 0) { av_packet_unref(pkt);
消耗掉以后就会在
decode_simple_internal
中调用av_packet_unref
。
第三个avcodec_send_packet
进来,因为前面没有解码消耗,avci->buffer_pkt->data
不为NULL,AVPACKET_IS_EMPTY
判断失败,返回EAGAIN。
所以在使用FFmpeg解码时候,avcodec_send_packet
和avcodec_receive_frame
得在一起出现,receive_frame需要及时的将frame从avci->buffer_frame
中取出。
- 对于没有B桢的视频,没有及时取走后,因为第二个packet没有被解码,第三个以上的buf进来都会报
EAGAIN
。 - 对于有B桢的视频,进去几桢之后,没有及时取走,后面就不会再调用decode解码,前面的IBBPBB的桢排布,在第五个buf进来的时候也会报
EAGAIN
。