一、引言
未加密的情况下,FLV文件中的一个Video Tag = Tag header + VideoTagHeader + VIDEODATA。在上一节《音视频入门基础:FLV专题(16)------FFmpeg源码中,解码Video Tag的VideoTagHeader的实现》中讲述了FFmpeg源码是怎样解码Video Tag的VideoTagHeader的。本文讲述FFmpeg源码在解码VideoTagHeader后,是怎样提取VIDEODATA的(以视频压缩编码格式为H.264为例)。
二、FFmpeg源码中,提取Video Tag的VIDEODATA的实现
从《音视频入门基础:FLV专题(15)------Video Tag简介》可以知道:当FLV文件的视频压缩编码格式为H.264并且未加密时,如果VideoTagHeader中的AVCPacketType的值为0,VIDEODATA为AVCDecoderConfigurationRecord;如果AVCPacketType的值为1,VIDEODATA包含一个或多个NALU;如果AVCPacketType的值为2,表示这段H.264码流结束,VIDEODATA没有数据。所以下面得分情况讨论。
(一)AVCPacketType的值为0
AVCPacketType的值为0时,VIDEODATA为AVCDecoderConfigurationRecord。flv_read_packet函数通过下面代码提取VIDEODATA:
cpp
if (type == 0 && (!st->codecpar->extradata || st->codecpar->codec_id == AV_CODEC_ID_AAC ||
st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_HEVC ||
st->codecpar->codec_id == AV_CODEC_ID_AV1 || st->codecpar->codec_id == AV_CODEC_ID_VP9)) {
AVDictionaryEntry *t;
if (st->codecpar->extradata) {
if ((ret = flv_queue_extradata(flv, s->pb, stream_type, size)) < 0)
return ret;
ret = FFERROR_REDO;
goto leave;
}
if ((ret = flv_get_extradata(s, st, size)) < 0)
return ret;
/* Workaround for buggy Omnia A/XE encoder */
t = av_dict_get(s->metadata, "Encoder", NULL, 0);
if (st->codecpar->codec_id == AV_CODEC_ID_AAC && t && !strcmp(t->value, "Omnia A/XE"))
st->codecpar->extradata_size = 2;
ret = FFERROR_REDO;
goto leave;
}
上面的代码块中,局部变量type存贮VideoTagHeader的AVCPacketType属性。当AVCPacketType值为1并且视频压缩编码格式为H.264并且还未获取avcC包装的H.264的extradata时,会执行:
cpp
if ((ret = flv_get_extradata(s, st, size)) < 0)
return ret;
而flv_get_extradata函数内部会调用ff_get_extradata函数:
cpp
static int flv_get_extradata(AVFormatContext *s, AVStream *st, int size)
{
//...
if ((ret = ff_get_extradata(s, st->codecpar, s->pb, size)) < 0)
// ...
}
ff_get_extradata函数内部会调用ffio_read_size函数:
cpp
int ff_get_extradata(void *logctx, AVCodecParameters *par, AVIOContext *pb, int size)
{
//...
ret = ffio_read_size(pb, par->extradata, size);
//...
}
ffio_read_size函数内部会调用avio_read函数。而根据《FFmpeg源码:avio_read函数分析》可以知道,avio_read函数的作用是:首先尝试从AVIOContext输入缓冲区中读取数据,如果输入缓冲区中没有数据或者数据已被读完或者读完后还不够size个字节,通过文件描述符去读取本地媒体文件中的数据或者通过socket接收网络流中的数据,保存到形参buf指向的缓冲区中:
cpp
int ffio_read_size(AVIOContext *s, unsigned char *buf, int size)
{
int ret = avio_read(s, buf, size);
//...
}
所以下面flv_get_extradata语句的作用是:读取该Video Tag的AVCDecoderConfigurationRecord(extradata),将其存贮到s->streams[stream_index]->codecpar->extradata指向的缓冲区中。其中stream_index为该路视频流在FLV文件中的流索引,size为extradata的大小(以字节为单位):
cpp
if ((ret = flv_get_extradata(s, st, size)) < 0)
return ret;
然后之后在flv_read_packet函数外部会通过ff_h264_decode_extradata函数解码上述提取出来的AVCDecoderConfigurationRecord(extradata),具体可以参考:《音视频入门基础:H.264专题(20)------FFmpeg源码中,解码AVCDecoderConfigurationRecord的实现》。
(二)AVCPacketType的值为1
AVCPacketType的值为1时,VIDEODATA包含一个或多个NALU。flv_read_packet函数通过下面代码提取VIDEODATA。即通过av_get_packet函数读取NALU数据(包含一个或多个NALU),保存到pkt->data指向的缓冲区中。关于av_get_packet函数可以参考:《FFmpeg源码:append_packet_chunked、av_get_packet、av_append_packet函数分析》。这样在执行下面的代码块后,pkt->data会得到该帧的实际的压缩后的H.264视频数据;pkt->dts会得到该帧的解码时间戳,解码时间戳来源于Tag header的Timestamp和TimestampExtended属性,具体可以参考:《音视频入门基础:FLV专题(8)------FFmpeg源码中,解码Tag header的实现》;pkt->pts会得到该帧的显示时间戳,显示时间戳等于dts + cts,具体可以参考:《音视频入门基础:FLV专题(16)------FFmpeg源码中,解码Video Tag的VideoTagHeader的实现》:
cpp
ret = av_get_packet(s->pb, pkt, size);
if (ret < 0)
return ret;
pkt->dts = dts;
pkt->pts = pts == AV_NOPTS_VALUE ? dts : pts;
pkt->stream_index = st->index;
pkt->pos = pos;
(三)AVCPacketType的值为2
AVCPacketType的值为2时,flv_read_packet函数通过下面代码提取VIDEODATA。由于AVCPacketType的值为2时表示这段H.264码流结束,VIDEODATA没有数据,所以直接goto leave不执行任何操作:
cpp
/* skip empty data packets */
if (!size) {
ret = FFERROR_REDO;
goto leave;
}