------------------------------------------------------------
author: hjjdebug
date: 2024年 07月 30日 星期二 13:22:07 CST
description: ts流音视频组包关键函数分析
------------------------------------------------------------
在av_read_frame(AVFormatContext *s, AVPacket *pkt) 中
先贴上一个网上的贴图.
这个图就只是一个框架,本博想说的是其更底层的数据组包的过程。
可认为是ff_read_packet() 接口下面的东西以ts流为例,它必需要通过mpegts_push_data 来进行内存分配,数据组装
提纲:
分析1: buffer_pool_get()
分析2: MpegTSContext 对象是唯一的吗?
分析3: 内存池中的数据,怎样转移到packet 中?
分析4: 为什么要用内存池,内存池中申请的内存,何时释放?
一个TS流的例子:
内存上的考虑: 调用栈
0 in posix_memalign of ld_preload_udpsend.c:268
1 in av_malloc of libavutil/mem.c:86
2 in av_mallocz of libavutil/mem.c:239
3 in av_buffer_pool_init of libavutil/buffer.c:268
4 in buffer_pool_get of libavformat/mpegts.c:1112
5 in mpegts_push_data of libavformat/mpegts.c:1193
6 in handle_packet of libavformat/mpegts.c:2846
7 in handle_packets of libavformat/mpegts.c:2975
8 in mpegts_read_packet of libavformat/mpegts.c:3219
9 in ff_read_packet of libavformat/utils.c:843
10 in read_frame_internal of libavformat/utils.c:1546
11 in av_read_frame of libavformat/utils.c:1750
12 in main of demux-decoding.c:372
当是pes 包时,会调用到下面的函数:
static int mpegts_push_data(MpegTSFilter *filter,
const uint8_t *buf, int buf_size, int is_start,
int64_t pos)
{
PESContext *pes = filter->u.pes_filter.opaque;
MpegTSContext *ts = pes->ts;
while (buf_size > 0) {
switch (pes->state) { //如果正在分析的包是pes 包头
case MPEGTS_HEADER:
...
pes->total_size = AV_RB16(pes->header + 4); //读取包的长度
/* NOTE: a zero total size means the PES size is unbounded */
if (!pes->total_size)
pes->total_size = MAX_PES_PAYLOAD;
/* allocate pes buffer */
pes->buffer = buffer_pool_get(ts, pes->total_size); //申请得到pes 缓冲位置
...
}
参数1: MpegTSFilter *filter, 对象
参数2: 输入,数据指针 buf.
参数3: 输入, 数据大小
参数4,bool 值,
参数5, 文件位置信息
------------------------------------------------------------
分析1: buffer_pool_get()
------------------------------------------------------------
static AVBufferRef *buffer_pool_get(MpegTSContext *ts, int size)
{
int index = av_log2(size + AV_INPUT_BUFFER_PADDING_SIZE); // 根据大小计算它是2的多少次幂
if (!ts->pools[index]) { //由多种pool, 如果该大小的pool 还没有创建
int pool_size = FFMIN(MAX_PES_PAYLOAD + AV_INPUT_BUFFER_PADDING_SIZE, 2 << index); // pool 的大小是2的整数次幂
// 创建这个pool, 这里只分配内存88 字节,是pool的头部大小
// 结果保存在ts 对象中.(一个MpegTSContext)
ts->pools[index] = av_buffer_pool_init(pool_size, NULL);
if (!ts->pools[index])
return NULL;
}
//这里当pool中没有空闲项时,会创建一个项,每项会通过av_malloc->posix_memalign向系统申请pool_size大小的数据
//当有空闲项时,直接返回就不用向系统申请了, pool 就是一个内存项的链表
return av_buffer_pool_get(ts->pools[index]);
}
内存池指针保存在MpegTSContext 对象中.
------------------------------------------------------------
分析2: MpegTSContext 对象是唯一的吗?
------------------------------------------------------------
先看一眼ts 吧, 它叫MpegTs 的上下文
(gdb) p ts
$13 = (MpegTSContext *) 0x55555556b3c0
MpegTSContext *ts = pes->ts;
它是从pes->ts 得来的.
而pes 是PESContext 对象, 可叫做基础流包上下文
PESContext *pes = filter->u.pes_filter.opaque;
而基础流包上下文又是从filter 得到的,filter 是MpegTSFilter对象
面向对象的编程思想,不怕你复杂,就想看看你的真身是怎么来的.有是怎么没的!
从handle_packet 怎样调用到mpegts_push_data()
static int handle_packet(MpegTSContext *ts, const uint8_t *packet, int64_t pos)
{
int pid = AV_RB16(packet + 1) & 0x1fff;
int is_start = packet[1] & 0x40;
MpegTSFilter *tss = ts->pids[pid];
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;
}
}
tss->u.pes_filter.pes_cb 就是mpegts_push_data, 函数指针就构成了架构的软调用。
而tss 就是传递下去的第一个参数. 可见MpegTSFilter 就是数据的关键!
看代码, tss = ts->pids[pid]
可见又绕了回来, tss 由 MpegTSContext 对象决定. 由包的pid就能找到对应的filter
MpegTSContext 对象由哪里来?
static int mpegts_read_packet(AVFormatContext *s, AVPacket *pkt)
{
MpegTSContext *ts = s->priv_data;
pkt->size = -1;
ts->pkt = pkt;
ret = handle_packets(ts, 0);
...
}
可见MpegTSContext 对象是AVFormatContext 对象的私有数据,
而AVFormatContext 对象在整个数据处理期间是唯一的,所以 MpegTSContext 对象也是唯一的.
至于它在哪里创建的,就先不关心了.
------------------------------------------------------------
分析3: 内存池中的数据,怎样转移到packet 中?
------------------------------------------------------------
0 in new_pes_packet of libavformat/mpegts.c:1026
1 in mpegts_push_data of libavformat/mpegts.c:1134
2 in handle_packet of libavformat/mpegts.c:2846
3 in handle_packets of libavformat/mpegts.c:2975
4 in mpegts_read_packet of libavformat/mpegts.c:3219
5 in ff_read_packet of libavformat/utils.c:843
6 in read_frame_internal of libavformat/utils.c:1546
7 in av_read_frame of libavformat/utils.c:1750
8 in main of demux-decoding.c:372
static int new_pes_packet(PESContext *pes, AVPacket *pkt)
{
av_packet_unref(pkt); //想往pkt中装东西,总是先去旧,再迎新.
pkt->buf = pes->buffer; //原来pkt数据都是从pes中接手的
pkt->data = pes->buffer->data;
pkt->size = pes->data_index;
memset(pkt->data + pkt->size, 0, AV_INPUT_BUFFER_PADDING_SIZE); //加上填充字节
pkt->stream_index = pes->st->index;
pkt->pts = pes->pts;
pkt->dts = pes->dts;
pkt->pos = pes->ts_packet_pos;
pkt->flags = pes->flags;
uinr8_t *sd = av_packet_new_side_data(pkt, AV_PKT_DATA_MPEGTS_STREAM_ID, 1);
*sd = pes->stream_id;
pes->buffer = NULL; // pes->buffer 变成了空,并被复位
reset_pes_packet_state(pes);
return 0;
}
虽然这里使用了pkt, 当要形成最终的pkt还要经过一断长长的路程,因为还要对pkt 进行parse_packet();
把pkt处理到s->internal->parse_pkt.释放掉pkt. 在这里申请的内存通过pool_release_buffer归还给了内存池.
还要把处理好的packet提交到链表中,再从链表中取回,这里限于篇幅就不展开了.
static int mpegts_push_data(MpegTSFilter *filter,
const uint8_t *buf, int buf_size, int is_start,
int64_t pos)
{
PESContext *pes = filter->u.pes_filter.opaque;
MpegTSContext *ts = pes->ts;
const uint8_t *p;
int ret, len, code;
if (is_start) { //正在分析的包是开始包
if (pes->state == MPEGTS_PAYLOAD && pes->data_index > 0) { //目前pes 正在填充es数据
//意味着上一个包已经结束,这是一个新的es包,立即把以前分析的pes包转移到pkt中,并创建新的pes包,
ret = new_pes_packet(pes, ts->pkt); //ts->pkt,就是上层调用传的参数pkt
if (ret < 0) return ret;
ts->stop_parse = 1;// 就是说找到了包尾,上层调用要处理.
} else {
reset_pes_packet_state(pes);
}
pes->state = MPEGTS_HEADER; //设置pes状态,处理完该包数据
pes->ts_packet_pos = pos;
}
....
}
------------------------------------------------------------
分析4: 为什么要用内存池,内存池中申请的内存,何时释放?
------------------------------------------------------------
内存池是一个内存项链表管理结构,程序向内存池申请内存项,如果没有空闲项,
内存池向系统申请内存。
当内存项使用完毕释放时(在read_frame中是在parse_packet函数中),
内存项通过 pool_release_buffer()归还给内存池而不是通过free()归还给系统
只有当当调用 av_buffer_pool_uninit(&pool); 并且pool的引用计数为0的时候,才会释放内存池,
把所有从系统申请的内存归还给系统.
实例可参考: FFMpeg AVBufferPool 的理解与掌握_ffmpeg 使用内存池-CSDN博客
小结: 着重分析了ff_read_packet中的一种实现,ts流的组包过程.
从基础流pes组包,内存分配的角度分析了mpegs_push_data()函数.重在理解而没有照抄代码.
重点是说内存池并没有随包的释放而释放给系统,其生命周期是整个数据分析周期.