------------------------------------------------------------
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的核心工作.
?