------------------------------------------------------------
author: hjjdebug
date: 2024年 07月 05日 星期五 11:02:51 CST
av_read_frame 代码研究
------------------------------------------------------------
有人只标注一层,标注一层太肤浅了.不能了解底层之精妙.
有人给出调用框图, 框图又太笼统了,还是了解不了细节.
如果来一个完全的标注,那也有两个毛病.
1. 代码太多,篇幅太大.
2. 重点不突出, 一叶障目,窥不见森林.
那怎么做呢?
捡一个最深的调用为纲,搞清其工作原理,再辅以上下的各个问题解释清楚。
做到纲举目张.
想读一个frame, 以ts文件为例, 它必需要读文件, 并且它要在
合适的位置停止读,因为数据已经构成了一个frame.
mpegts.c 就是ts 文件执行读写和分析的基础文件
我在mpegts.c 中设下了一个断点.
0 in mpegts_push_data of libavformat/mpegts.c:1377
1 in handle_packet of libavformat/mpegts.c:2846
2 in handle_packets of libavformat/mpegts.c:2975
3 in mpegts_read_packet of libavformat/mpegts.c:3219
4 in ff_read_packet of libavformat/utils.c:843
5 in read_frame_internal of libavformat/utils.c:1546
6 in av_read_frame of libavformat/utils.c:1750
7 in main of main.c:304
上层框架简单,就是接口,调用别人干活,主要看下层,是具体的操作:
handel_packes:
它循环调用读包, 每次读188字节,处理包 handle_packet, 满足条件才会退出.
handle_packet:
根据包的pid 可以拿到它的过滤器
MpegTsFilter tss = ts->pids[pid];
过滤器的类型也是在分析section 数据时建立的,例如pes 的过滤器是分析pmt表时建立的.
pmt表中记录了每个节目包含哪些流(最常见的是一个音频,一个视频), 这些流的ID 是多少
如果过滤器的类型是PES, 就是说该小包数据是pes,就会回调pes_cb, 这就是mpegts_push_data
if (tss->type == MPEGTS_PES)
{
if ((ret = tss->u.pes_filter.pes_cb(tss, p, p_end - p, is_start,
pos - ts->raw_packet_size)) < 0)
return ret;
}
就是说从id拿到对应的id对象(tss),然后调用对应的回调函数tss->u.pes_filter.pes_cb = mpegts_push_data
可见pes_cb并不是tss的直接成员,而是其下成员u.pes_filter的一个成员. 又多了一个中间管理者pes_filter
可以认为pes_filter和section_filter 是并列的,是一个联合.
union {
MpegTSPESFilter pes_filter;
MpegTSSectionFilter section_filter;
} u;
而tss是它们的上一级.
struct MpegTSFilter {
int pid;
int es_id;
int last_cc; /* last cc code (-1 if first packet) */
int64_t last_pcr;
int discard;
enum MpegTSFilterType type;
union {
MpegTSPESFilter pes_filter;
MpegTSSectionFilter section_filter;
} u;
};
mpegts_push_data:
它大部分时间是把负载直接copy到pes 数据区,但当数据copy够了时,就设置退出条件.
所谓copy够就是拷贝的pes数据pes->data_index 加头部大小pes->header_size等于
pes包总大小加上pes包起始大小
switch(pes->state)
{
case MPEGTS_PAYLOAD
memcpy(pes->buffer->data + pes->data_index, p, buf_size);
pes->data_index += buf_size;
if (!ts->stop_parse && pes->total_size < MAX_PES_PAYLOAD &&
pes->pes_header_size + pes->data_index == pes->total_size + PES_START_SIZE)
{ //数据copy够了,退出
ts->stop_parse = 1;
ret = new_pes_packet(pes, ts->pkt); //后面如果还有数,那就属于新包数据了
if (ret < 0)
return ret;
}
}
//下面是 gdb 打印的某一次结果
(gdb) p pes->pes_header_size
$20 = 14
(gdb) p pes->data_index //此处是负载的位置索引
$21 = 2304
(gdb) p pes->total_size
$22 = 2312
14+2304 = 2312 + 6
下面我们来分析一个这几个概念.
1. #define PES_START_SIZE 6
pes包时由固定的3个字节00 00 01 及后面3个字节stream_id(1)+包长度(packet_length)
2. pes->total_size
这就是上面pes 头部中的packet_length. 从码流中得到的大小
它在代码中什么位置赋值的?
pes->total_size = AV_RB16(pes->header + 4);
if (!pes->total_size)
pes->total_size = MAX_PES_PAYLOAD; //200*1024
//当packet_length==0 时, 我们给最大的尺寸200K , 此时真正的大小要等到再遇到一个pes包头来确定大小.
/* allocate pes buffer */
pes->buffer = buffer_pool_get(ts, pes->total_size);
if (!pes->buffer)
return AVERROR(ENOMEM);
3. pes->pes_header_size
if (pes->data_index == PES_HEADER_SIZE) //#define PES_HEADER_SIZE 9
{
pes->pes_header_size = pes->header[8] + 9; //9字节是固定头,前面6字节是start头,后面3字节是可选头部,
pes->state = MPEGTS_PESHEADER_FILL;
}
其中:
pes->header[6], 加扰说明,版权说明等
pes->header[7], 7个标志位,说明是否有option跟随,例如pts,dts等等
pes->header[8], 可选头部的长度,1byte 不会超过255
头部大小范围,最大 MAX_PES_HEADER_SIZE (9+255)
4. pes->data_index
当然是pes数据的位置索引了. 不过它的身份是变的, 一会是数据的索引,一会是负载的索引, 要看它的时机.
变量吗,就是变着来, 不过这样容易混淆,用是简单了,读却费劲了. 最好是分两个或多个变量.角色应该唯一.
就像变量i, 一会是这个的索引,一会是那个的索引,只要分的清,也可以不换名,因为用着简单.
小结:
读一个frame, 就是从原始数据中不停的读, 读到一个合适的位置停止读,把有效的数据作为一个frame反馈给上层.
当然,如果你不是从数据文件中读,而是内存中已经有了,例如以链表形式或者数组形式,那copy给你就可以了.
这就是av_read_frame的核心工作.