av_parser_parse2 与 h264_parse 函数分析
文章摘要: 本文分析了FFmpeg中的av_parser_parse2函数和h264_parse功能。av_parser_parse2作为接口层函数,其核心功能是通过解析器上下文(AVCodecParserContext)调用底层解析器实现数据分帧。文章详细解读了函数参数含义,包括输入/输出缓冲区、编解码器上下文等,并剖析了函数实现逻辑,指出其核心是通过s->parser->parse
author: hjjdebug
date : 2025年 11月 26日 星期三 15:20:10 CST
descrip: av_parser_parse2 与 h264_parse 函数分析
数据分析, 就是要分析每一个byte 的数据, 区分出数据的意义. av_parser_parse2就是一个数据分析函数.
对代码的理解,也必需要对每一条语句都要清楚,才算理解了代码. 所以我也倾向于开始标注代码了.
1 av_parser_parse2 函数原型:
int len = av_parser_parse2(pCodecParserCtx, pCodecCtx,
&pPacket.data, &pPacket.size, //输出
pCurBuf, dwCurBufSize, //输入
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); //后面3参数不重要,用来填充当前frame时间位置信息
它的目的, 就是把所有的输入数据, 划分出一个frame 来放入pPacket.data
1.0: 为什么叫parse2.
ffmpeg 中一般带2的函数名称一般是代表这个函数是个接口层,还有一层是实现层.
为什么要分两层? 分两层的优点,缺点是什么? 后面慢慢再说.
1.1 第一个参数是什么?
是codec parse context 对象.
AVCodecParserContext* pCodecParserCtx = av_parser_init(vDecoderID);//根据解码器类型获取解析器上下文
什么叫解析器上下文?
所谓解析器上下文就是一个对象, 给你一个解析器, 以后你把解析的结果放到这个上下文中.
什么是对象? 对象就是一块内存区域, 它不仅能够保存属性(数据), 而且还能够保存函数指针(行为).
什么是解析器? 解析器也是一个对象. 它能够对特定的数据进行分析.
可见, 解析器上下文包含着一个解析器.
1.2 第二个参数是什么?
是解码器上下文.
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);//根据解码器分配一个解码器上下文
可见, 编解码器上下文包含着一个编解码器.
什么叫编解码器上下文?
所谓编解码器上下文就是一个对象, 给你一个编解码器, 以后你编解码的结果放到这个上下文中.
什么是编解码器? 编解码器或者是一个编码器,或者是一个解码器, 编解码器也是一个对象. 它能够对特定的数据进行编码或者解码.
怎么跟第一个参数解析器上下文如此想像呢? 是的,就是这么像.
计算机处理的就是内存数据,计算机也就只认识对象了. 而对象往往是一个包含着一个.
尤其是c语言, 它有指针的概念, 一个指针往往就指向了一个对象.
1.3 其它数据
第3,4 参数是输出数据,
第5,6 参数是输入数据.
3,4,5,6参数容易理解, 就是给一块输入数据, 看看能不能得到输出数据.
第7,8,9 参数暂时不用关心
1.4 函数代码分析
int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx,
uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size,
int64_t pts, int64_t dts, int64_t pos)
{
int index, i;
//断言 avctx->codec_id
av_assert1(avctx->codec_id != AV_CODEC_ID_NONE);
av_assert1(avctx->codec_id == s->parser->codec_ids[0] ||
avctx->codec_id == s->parser->codec_ids[1] ||
avctx->codec_id == s->parser->codec_ids[2] ||
avctx->codec_id == s->parser->codec_ids[3] ||
avctx->codec_id == s->parser->codec_ids[4] ||
avctx->codec_id == s->parser->codec_ids[5] ||
avctx->codec_id == s->parser->codec_ids[6]);
//设置s->flags, 加上offset
if (!(s->flags & PARSER_FLAG_FETCHED_OFFSET)) {
s->next_frame_offset = s->cur_offset = pos;
s->flags |= PARSER_FLAG_FETCHED_OFFSET;
}
uint8_t dummy_buf[AV_INPUT_BUFFER_PADDING_SIZE];
//根据输入数据的长度进行判断,为0时让buf指向dummy_buf
if (buf_size == 0) {
/* padding is always necessary even if EOF, so we add it here */
memset(dummy_buf, 0, sizeof(dummy_buf));
buf = dummy_buf;
}
else if (s->cur_offset + buf_size != s->cur_frame_end[s->cur_frame_start_index]) {
/* add a new packet descriptor */
i = (s->cur_frame_start_index + 1) & (AV_PARSER_PTS_NB - 1); //AV_PARSER_PTS_NB 是4, 即i 是0,1,2,3的循环
s->cur_frame_start_index = i;
s->cur_frame_offset[i] = s->cur_offset;
s->cur_frame_end[i] = s->cur_offset + buf_size;
s->cur_frame_pts[i] = pts;
s->cur_frame_dts[i] = dts;
s->cur_frame_pos[i] = pos; // 关于pts,dts,pos , 现在我们先不考虑
}
if (s->fetch_timestamp) {
s->fetch_timestamp = 0; //时间戳的问题,先不考虑
s->last_pts = s->pts;
s->last_dts = s->dts;
s->last_pos = s->pos;
ff_fetch_timestamp(s, 0, 0, 0);
}
/* WARNING: the returned index can be negative */
index = s->parser->parser_parse(s, avctx, (const uint8_t **) poutbuf,
poutbuf_size, buf, buf_size);
#define FILL(name) if(s->name > 0 && avctx->name <= 0) avctx->name = s->name
if (avctx->codec_type == AVMEDIA_TYPE_VIDEO) {
//从parser 向 codec context 填充数据
FILL(field_order);
FILL(coded_width);
FILL(coded_height);
FILL(width);
FILL(height);
}
/* update the file pointer */
if (*poutbuf_size)
{ //如果输出返回了分析的数据长度,更新frame_offset 和 next_frame_offset
// index, 是实现类函数的返回值,代表了消耗的数据长度
s->frame_offset = s->next_frame_offset;
s->next_frame_offset = s->cur_offset + index;
s->fetch_timestamp = 1;
}
else
{
*poutbuf = NULL;//没有分析到完整包,给指针为空
}
if (index < 0)
index = 0;
s->cur_offset += index; //更新或者不更新s->cur_offset
return index; //返回实现类返回的数据长度
}
看完代码发现, 核心实现要看s->parser->parser_parse() 函数的执行结果. 其它代码都注释了,也没什么.
这就是第一层接口为什么叫parse2 的原因, 它告诉你,真正的实现在下一层.
生活中有很多这样的例子, 接口和实现是分开的.
接待你的是一个人,真正提供服务的是另外一个人. 你可以自由发挥想象一下!
为什么要这样做呢? 因为对方可提供的服务多啊.
缺点是什么? 缺点是我还得要找需要的服务啊.
例如这里, 给你一个.h264格式的文件, 就要先找到h264_parser
不过我这里使用的是parser context, 那也行, 像这样创建context
AVCodecParserContext* pCodecParserCtx = av_parser_init(vDecoderID);//根据解码器类型获取解析器上下文
按图索骥, 给我一点信息,我给你找到你需要的东西.
其中vDecoderID = AV_CODEC_ID_H264;
就是说, 给我一个ID, 我给你一个parse_context.
同样另一路变化:
const AVCodec *pCodec = avcodec_find_decoder(vDecoderID);//获取指定类型的解码器, parser 需要codec_ctx
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);//根据解码器分配一个解码器上下文
给我ID, 我能找到AVCodec, 给据AVCodec, 我能分配CodecContext.
这就是从多个对象中挑选出一个来,你必需要付出的代价, 管理的代价.
有多少大好时光就花费了这种查找上, 绕来扰去, 原来就是为了查找一个对象. 想象一下,
你结婚之前,人生前20年都花在找对象上@^@,浪费啊!
2 实现类分析.
2.1 函数原型
index = s->parser->parser_parse(s, avctx, (const uint8_t **) poutbuf,
poutbuf_size, buf, buf_size);
2.2 s->parser 是何时赋值的? (实现类对象是如何赋值的)
我们跟踪一下av_parser_init(), 就知道它是枚举了每一个parser 对象, 看其id是否与
传来的参数vDecoderID 一致, 一致就是找到了这个parser, 然后把指针付给s
void *i=0;
while ((parser = av_parser_iterate(&i))) {
if (parser->codec_ids[0] == codec_id ||
parser->codec_ids[1] == codec_id ||
parser->codec_ids[2] == codec_id ||
parser->codec_ids[3] == codec_id ||
parser->codec_ids[4] == codec_id ||
parser->codec_ids[5] == codec_id ||
parser->codec_ids[6] == codec_id)
goto found;
}
return NULL;
fount:
s = av_mallocz(sizeof(AVCodecParserContext));
s->parser = parser;
2.3 s->parser->parser_parse 是何时赋值的? 会具体指向哪里?
找到了parser 对象, 则对象中的parser_parse 函数就是实现好的. 这就是别人给你写好的代码.
我们看一下 ff_h264_parser 对象的例子.
(gdb) p parser
$3 = (const AVCodecParser *) 0x7ffff78892c0 <ff_h264_parser>
0x7ffff78892c0 这个内存地址,处于/usr/local/lib/libavcodec.so.60.31.102模块地址空间.
说明它是该模块定义的一个全局地址,
对方给我们提供了全局对象.
具体内容:
(gdb) p *parser
$4 = {
codec_ids = {27, 0, 0, 0, 0, 0, 0},
priv_data_size = 14024,
parser_init = 0x7ffff6a991f3 <init>,
parser_parse = 0x7ffff6a98c13 <h264_parse>,
parser_close = 0x7ffff6a9919f <h264_close>,
split = 0x0
}
找找它所处的文件: 在 libavcodec/h264_parser.c 中688行,有
const AVCodecParser ff_h264_parser = {
.codec_ids = { AV_CODEC_ID_H264 },
.priv_data_size = sizeof(H264ParseContext),
.parser_init = init,
.parser_parse = h264_parse,
.parser_close = h264_close,
};
这就是AVCodecParser 对象. 面向对象的编程方法. 连属性带方法都有了.
不仅仅是想你提供了编程函数. 二是向你提供了对象.
3 h264_parse 是怎样工作的?
最终干活的是h264_parse
3.1 函数原型
static int h264_parse(AVCodecParserContext *s,
AVCodecContext *avctx,
const uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size)
我们看到, 实现类函数参数与接口类函数参数是一样的.
3.2 代码注释
static int h264_parse(AVCodecParserContext *s,
AVCodecContext *avctx,
const uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size)
{
H264ParseContext *p = s->priv_data;
ParseContext *pc = &p->pc;
AVRational time_base = { 0, 1 };
int next;
if (!p->got_first) {
p->got_first = 1;
if (avctx->extradata_size) { // 测试代码这里 extradata_size=0
ff_h264_decode_extradata(avctx->extradata, avctx->extradata_size,
&p->ps, &p->is_avc, &p->nal_length_size,
avctx->err_recognition, avctx);
}
}
if (s->flags & PARSER_FLAG_COMPLETE_FRAMES) { //这里条件为假
next = buf_size;
}
else
{ //buf,buf_size 是输入数据,判断是否到帧尾,返回值是next
next = h264_find_frame_end(p, buf, buf_size, avctx);
//把数据与上下文进行合并, 返回值<0 就是没有形成frame
if (ff_combine_frame(pc, next, &buf, &buf_size) < 0) {
*poutbuf = NULL;
*poutbuf_size = 0;
return buf_size;
}
//如果next 是负值, 把它与pc->last_index 相加,再一次查找frame_end
if (next < 0 && next != END_NOT_FOUND) {
av_assert1(pc->last_index + next >= 0);
h264_find_frame_end(p, &pc->buffer[pc->last_index + next], -next, avctx); // update state
}
}
//分析网络抽象层(net abtract layer)单元
parse_nal_units(s, avctx, buf, buf_size); //这个函数就不展开了,它会继续完善s,avctx中的成员
if (avctx->framerate.num) // 从framerate 计算时基
time_base = av_inv_q(av_mul_q(avctx->framerate, (AVRational){2, 1}));
if (p->sei.picture_timing.cpb_removal_delay >= 0) {
//sei 补充增强信息(supplementa enhancement information),非必要的信息,例如人物介绍,节目介绍等信息,不影响播放
s->dts_sync_point = p->sei.buffering_period.present;
s->dts_ref_dts_delta = p->sei.picture_timing.cpb_removal_delay;
s->pts_dts_delta = p->sei.picture_timing.dpb_output_delay;
} else {
s->dts_sync_point = INT_MIN;
s->dts_ref_dts_delta = INT_MIN;
s->pts_dts_delta = INT_MIN;
}
if (s->flags & PARSER_FLAG_ONCE) {
s->flags &= PARSER_FLAG_COMPLETE_FRAMES;
}
if (s->dts_sync_point >= 0) {
int64_t den = time_base.den * (int64_t)avctx->pkt_timebase.num;
if (den > 0) { // 时间戳信息
int64_t num = time_base.num * (int64_t)avctx->pkt_timebase.den;
if (s->dts != AV_NOPTS_VALUE) {
// got DTS from the stream, update reference timestamp
p->reference_dts = av_sat_sub64(s->dts, av_rescale(s->dts_ref_dts_delta, num, den));
} else if (p->reference_dts != AV_NOPTS_VALUE) {
// compute DTS based on reference timestamp
s->dts = av_sat_add64(p->reference_dts, av_rescale(s->dts_ref_dts_delta, num, den));
}
if (p->reference_dts != AV_NOPTS_VALUE && s->pts == AV_NOPTS_VALUE)
s->pts = s->dts + av_rescale(s->pts_dts_delta, num, den);
if (s->dts_sync_point > 0)
p->reference_dts = s->dts; // new reference
}
}
*poutbuf = buf;
*poutbuf_size = buf_size; //关键是这里,输出的buf数据大小
return next;
}
//分析上面函数,我们知道,关键是下面2个函数
h264 查找frame_end, 它会枚举每一个输入byte, 是一个状态机转换.去搜索frame 尾部
7 - 初始化状态
2 - 找到1个0
1 - 找到2个0
0 - 找到大于等于3个0
4 - 找到2个0和1个1,即001(即找到了起始码)
5 - 找到至少3个0和1个1,即0001等等(即找到了起始码)
>=8 - 找到l了Slice Header, 要继续找frame_end
3.2.1 h264_find_frame_end() 函数注释
static int h264_find_frame_end(H264ParseContext *p, const uint8_t *buf,
int buf_size, void *logctx)
{
int i, j;
uint32_t state;
ParseContext *pc = &p->pc;
//为next_avc 赋值
int next_avc = p->is_avc ? 0 : buf_size; //更具codec 是否是avc1赋值 next_avc, 我的测试流is_avc为0
state = pc->state;
if (state > 13)
state = 7;
if (p->is_avc && !p->nal_length_size)
av_log(logctx, AV_LOG_ERROR, "AVC-parser: nal length size invalid\n");
// 枚举每一个输入数据!!
for (i = 0; i < buf_size; i++) {
if (i >= next_avc) {
int64_t nalsize = 0;
i = next_avc;
for (j = 0; j < p->nal_length_size; j++)
nalsize = (nalsize << 8) | buf[i++];
if (!nalsize || nalsize > buf_size - i) {
av_log(logctx, AV_LOG_ERROR, "AVC-parser: nal size %"PRId64" "
"remaining %d\n", nalsize, buf_size - i);
return buf_size;
}
next_avc = i + nalsize;
state = 5;
}
// 状态为7, 初始化状态.
//下面代码就是查找 001或0001 的候选位置,
if (state == 7) {
//参考后边 查找字节0 的代码
i += p->h264dsp.startcode_find_candidate(buf + i, next_avc - i);//实际是查找下一个byte 为0的位置
if (i < next_avc)
state = 2; //转入状态2
} else if (state <= 2) { //状态0,1,2
if (buf[i] == 1) //语句字节,进行状态机转换
state ^= 5; // 2->7, 1->4, 0->5
else if (buf[i])
state = 7;
else
state >>= 1; // 2->1, 1->0, 0->0
} else if (state <= 5) { //状态3,4,5
int nalu_type = buf[i] & 0x1F; //判断单元类型
if (nalu_type == H264_NAL_SEI || nalu_type == H264_NAL_SPS ||
nalu_type == H264_NAL_PPS || nalu_type == H264_NAL_AUD) {
if (pc->frame_start_found) {
i++;
goto found; //如果在查找frame, 则frame 已经找到(当为SPS(序列参数集),PPS(图像参数集),SEI(增强信息),AUD(分割符)
}
} else if (nalu_type == H264_NAL_SLICE || nalu_type == H264_NAL_DPA ||
nalu_type == H264_NAL_IDR_SLICE) { //切片, DPA, 状态加8
state += 8;
continue;
}
state = 7;
} else { //其它状态
unsigned int mb, last_mb = p->parse_last_mb;
GetBitContext gb;
//取得这个byte, 进行bit 分析
p->parse_history[p->parse_history_count++] = buf[i];
init_get_bits(&gb, p->parse_history, 8*p->parse_history_count);
//获取哥伦布长度
mb= get_ue_golomb_long(&gb);
//获取bit值
if (get_bits_left(&gb) > 0 || p->parse_history_count > 5) {
p->parse_last_mb = mb;
if (pc->frame_start_found)
{
//满足条件,找到了frame end
if (mb <= last_mb) {
i -= p->parse_history_count - 1;
p->parse_history_count = 0;
goto found;
}
} else
pc->frame_start_found = 1;
//条件不满足,继续查找
p->parse_history_count = 0;
state = 7;
}
}
}
pc->state = state;
if (p->is_avc)
return next_avc;
return END_NOT_FOUND;
found:
pc->state = 7;
pc->frame_start_found = 0;
if (p->is_avc) // codec 是否是avc1
return next_avc;
return i - (state & 5);
}
//查找字节0 的代码
int ff_startcode_find_candidate_c(const uint8_t *buf, int size)
{
int i = 0;
for (; i < size; i++)
if (!buf[i])
break;
return i;
}
3.2.2 ff_combine_frame() 函数注释
int ff_combine_frame(ParseContext *pc, int next,
const uint8_t **buf, int *buf_size)
{
pc->last_index = pc->index;
//没有找到frame 尾
if (next == END_NOT_FOUND) {
//把旧内存pc->buffer扩大到新尺寸(pc->index+*buf_size+padding),并保留大小到pc->buffer_size
void *new_buffer = av_fast_realloc(pc->buffer, &pc->buffer_size,
*buf_size + pc->index +
AV_INPUT_BUFFER_PADDING_SIZE);
pc->buffer = new_buffer; //保留新buf指针
memcpy(&pc->buffer[pc->index], *buf, *buf_size); //copy 数据
pc->index += *buf_size; //pc->index 更新
return -1;
}
//找到了frame 尾, pc->index + next 为新大小
*buf_size = pc->overread_index = pc->index + next;
/* append to buffer */
if (pc->index) {
void *new_buffer = av_fast_realloc(pc->buffer, &pc->buffer_size,
next + pc->index +
AV_INPUT_BUFFER_PADDING_SIZE);
pc->buffer = new_buffer;
if (next > -AV_INPUT_BUFFER_PADDING_SIZE)
memcpy(&pc->buffer[pc->index], *buf,
next + AV_INPUT_BUFFER_PADDING_SIZE); //追加到buffer
pc->index = 0;
*buf = pc->buffer;
}
return 0;
}
4. 完整测试代码:
参考该博客下给出的测试代码
其它问题:
pict_type 图片类型是在哪里赋值的?
我跟踪的一下, 它是在找到包尾后, 调用parse_nal_units()
来设置 pCodecParserCtx->pict_type
parse_nal_units() 是一个复杂函数,它与找frame_end 没有关系, 在本博中没有分析.
parse_nal_unit 还会解析设置picture order count POC 的数值,以后有机会再分析之.
由此测试程序,也可以测试aac 音频数据格式,或者其它你感兴趣的格式等等.
ffmpeg 各种分析器代码都写好了,只需我们测试调试一下,从中就能了解到它们的数据结构.
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)