FFmpeg源码:PacketList结构体、avpriv_packet_list_put、avpriv_packet_list_get、avpriv_packet_list_free函数分析

=================================================================

AVPacket结构体和其相关的函数分析:

FFmpeg存放压缩后的音视频数据的结构体:AVPacket简介

FFmpeg源码:av_init_packet、get_packet_defaults、av_packet_alloc函数分析

FFmpeg源码:av_packet_free_side_data、av_packet_unref、av_packet_free函数分析

FFmpeg源码:packet_alloc、av_new_packet、av_shrink_packet、av_grow_packet函数分析

FFmpeg源码:av_packet_move_ref、av_packet_make_refcounted函数分析

FFmpeg源码:PacketList结构体、avpriv_packet_list_put、avpriv_packet_list_get、avpriv_packet_list_free函数分析

FFmpeg源码:append_packet_chunked、av_get_packet函数分析

FFmpeg中调用av_read_frame函数导致的内存泄漏问题

使用vs诊断工具检测FFmpeg的内存泄漏问题

=================================================================

一、引言

FFmpeg源码的avformat_find_stream_info函数中,通过read_frame_internal函数读取到一帧AVPacket数据后,会通过avpriv_packet_list_put函数把该数据插入到一个单链表(PacketList)的尾部。然后在av_read_frame函数中, 会通过avpriv_packet_list_get函数从链表PacketList中取出该AVPacket数据。跟数组相比,链表的优点是空间没限制,插入删除元素快。这里需要频繁插入和删除元素(AVPacket数据),且对访问效率要求不高(完全不需要访问链表里面的AVPacket数据),所以针对该场景FFmpeg源码中使用了单链表。

二、单链表的基本概念

FFmpeg源码中使用PacketList结构体来实现存放AVPacket数据的单链表。这里先帮大家复习一下单链表的基本概念:

单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

以“结点的序列”表示线性表称作线性链表(单链表),单链表是链式存取的结构。


1.首结点:第一个有效结点
2.尾结点:最后一个有效结点
3.头结点:头结点的数据类型和首结点的类型一样;第一个有效结点之前的结点;头结点并不存放有效数据;加头结点的目的主要是为了方便对链表的操作
4.头指针:指向头结点的指针变量
5.尾指针:指向尾结点的指针变量

三、PacketListEntry和PacketList结构体

PacketListEntry和PacketList结构体声明在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的头文件libavcodec/packet_internal.h中:

typedef struct PacketListEntry {
    struct PacketListEntry *next;
    AVPacket pkt;
} PacketListEntry;

typedef struct PacketList {
    PacketListEntry *head, *tail;
} PacketList;

其中PacketListEntry是单链表中的结点。

PacketListEntry中的成员next是结点的指针域,指向该结点的后面一个结点。

PacketListEntry中的成员pkt是结点的数据域,存放实际的AVPacket数据。

PacketList可以认为是单链表本身。

PacketList中的成员head是头指针,但它指向的是首结点,而不是头结点。通过avpriv_packet_list_put函数创造出来的链表没有头结点。

PacketList中的成员tail是尾指针,指向尾结点。该尾指针的作用是用来找到整个链表,减少时间复杂度。通过avpriv_packet_list_put函数进行链表的插入操作是在表尾进行,头指针head因为一开始指向首结点,要找到尾结点的时间复杂度为O(n),而尾指针指向的就是尾结点,所以找到尾结点的时间复杂度为O(1)。关于时间复杂度的概念可以参考:《时间复杂度 O(1)》。

四、avpriv_packet_list_put函数

(一)avpriv_packet_list_put函数的声明

avpriv_packet_list_put函数声明在头文件libavcodec/packet_internal.h中:

/**
 * Append an AVPacket to the list.
 *
 * @param list  A PacketList
 * @param pkt   The packet being appended. The data described in it will
 *              be made reference counted if it isn't already.
 * @param copy  A callback to copy the contents of the packet to the list.
                May be null, in which case the packet's reference will be
                moved to the list.
 * @return 0 on success, negative AVERROR value on failure. On failure,
           the packet and the list are unchanged.
 */
int avpriv_packet_list_put(PacketList *list, AVPacket *pkt,
                           int (*copy)(AVPacket *dst, const AVPacket *src),
                           int flags);

该函数作用是:将形参pkt指向的AVPacket数据插入形参list指向的链表的尾部。执行该函数后,如果该AVPacket数据还没被引用计数,它会被引用计数。

形参list:输出型参数,指向一个PacketList类型的链表。

形参pkt:输入型参数,指向需要被插入的AVPacket数据。如果没有定义copy函数即avpriv_packet_list_put函数的第三个参数为NULL时,形参pkt指向的AVPacket会被“移动”(所有权从pkt指向的AVPacket对象转移)到链表的AVPacket对象中,也就是执行avpriv_packet_list_put函数前pkt->buf、pkt->data、pkt->side_data本来是指向包含数据的缓冲区的,但现在指向NULL了。所以执行avpriv_packet_list_put函数后不能再继续使用pkt指向的AVPacket了。

形参copy:函数指针,指向一个回调函数。自己设计该回调函数可以定义把AVPacket数据拷贝/移动到链表的方式。如果为NULL,表示是通过“移动”的方式把AVPacket数据插入到链表中。

形参flags:值一般为0,该参数没有意义,可忽略。

返回值:返回0表示成功,返回负数表示失败。

(二)avpriv_packet_list_put函数的定义

avpriv_packet_list_put函数定义在源文件libavcodec/avpacket.c中:

int avpriv_packet_list_put(PacketList *packet_buffer,
                           AVPacket      *pkt,
                           int (*copy)(AVPacket *dst, const AVPacket *src),
                           int flags)
{
    PacketListEntry *pktl = av_malloc(sizeof(*pktl));
    int ret;

    if (!pktl)
        return AVERROR(ENOMEM);

    if (copy) {
        get_packet_defaults(&pktl->pkt);
        ret = copy(&pktl->pkt, pkt);
        if (ret < 0) {
            av_free(pktl);
            return ret;
        }
    } else {
        ret = av_packet_make_refcounted(pkt);
        if (ret < 0) {
            av_free(pktl);
            return ret;
        }
        av_packet_move_ref(&pktl->pkt, pkt);
    }

    pktl->next = NULL;

    if (packet_buffer->head)
        packet_buffer->tail->next = pktl;
    else
        packet_buffer->head = pktl;

    /* Add the packet in the buffered packet list. */
    packet_buffer->tail = pktl;
    return 0;
}

该函数内部,首先通过av_malloc函数(关于av_malloc函数用法可以参考FFmpeg中内存分配和释放相关的源码:av_malloc函数、av_mallocz函数、av_free函数和av_freep函数分析)创建新结点,给新结点分配内存。如果申请内存失败,返回AVERROR(ENOMEM):

PacketListEntry *pktl = av_malloc(sizeof(*pktl));
int ret;

if (!pktl)
    return AVERROR(ENOMEM);

avpriv_packet_list_put函数的形参copy为NULL的情况下,会执行下面的代码块。通过av_packet_make_refcounted函数确保pkt指向的AVPacket对象描述的数据是引用计数的。然后通过av_packet_move_ref函数将形参pkt指向的AVPacket对象的所有属性移动到新结点的AVPacket对象中(关于av_packet_make_refcounted函数和av_packet_move_ref函数的用法可以参考《FFmpeg源码:av_packet_move_ref、av_packet_make_refcounted函数分析》):

ret = av_packet_make_refcounted(pkt);
if (ret < 0) {
    av_free(pktl);
    return ret;
}
av_packet_move_ref(&pktl->pkt, pkt);

PacketList只是一个普通单链表,不是循环单链表,所以让新结点的指针域指向NULL,而不是首结点:

pktl->next = NULL;

情况1:如果packet_buffer->head为空,表示该链表为空,链表中一个结点也没有,此时刚刚新创建的结点就是首结点,让头指针指向首结点,让尾指针指向尾结点,由于此时链表中只有一个首结点,所以此时首结点也是尾结点,让尾指针指向首结点;

情况2:如果packet_buffer->head非空,表示该链表中存在结点,让尾指针指向的尾结点中的指针域指向刚刚新创建的结点,然后让尾指针指向这个新结点,从而实现在链表的尾部插入AVPacket数据:

if (packet_buffer->head)
    packet_buffer->tail->next = pktl;
else
    packet_buffer->head = pktl;

/* Add the packet in the buffered packet list. */
packet_buffer->tail = pktl;

从上面可以看出来,通过尾指针可以快速找到链表的尾结点,减少在链表尾部进行插入操作的时间复杂度。

五、avpriv_packet_list_get函数

(一)avpriv_packet_list_get函数的声明

avpriv_packet_list_get函数声明在头文件libavcodec/packet_internal.h中:

/**
 * Remove the oldest AVPacket in the list and return it.
 *
 * @note The pkt will be overwritten completely on success. The caller
 *       owns the packet and must unref it by itself.
 *
 * @param head A pointer to a PacketList struct
 * @param pkt  Pointer to an AVPacket struct
 * @return 0 on success, and a packet is returned. AVERROR(EAGAIN) if
 *         the list was empty.
 */
int avpriv_packet_list_get(PacketList *list, AVPacket *pkt);

该函数的作用是:从形参list指向的链表的首部取出一个AVPacket数据,然后在链表中删除它对应的结点。

形参list:既是输入型参数也是输出型参数,指向一个PacketList类型的链表。

形参pkt:输出型参数,执行该函数后,pkt指向的AVPacket变量值(包括它的成员指针和内部缓冲区)会变为从链表首部取出的数据。

返回值:返回0表示成功,返回负数表示失败。

(二)avpriv_packet_list_get函数的定义

avpriv_packet_list_get函数定义在源文件libavcodec/avpacket.c中:

int avpriv_packet_list_get(PacketList *pkt_buffer,
                           AVPacket      *pkt)
{
    PacketListEntry *pktl = pkt_buffer->head;
    if (!pktl)
        return AVERROR(EAGAIN);
    *pkt        = pktl->pkt;
    pkt_buffer->head = pktl->next;
    if (!pkt_buffer->head)
        pkt_buffer->tail = NULL;
    av_freep(&pktl);
    return 0;
}

该函数内部,首先通过链表的头指针找到首结点。如果头指针为空,表示该链表为空,链表中一个结点也没有,此时返回AVERROR(EAGAIN)表示从链表取出数据失败:

PacketListEntry *pktl = pkt_buffer->head;
if (!pktl)
    return AVERROR(EAGAIN);

将首结点中的AVPacket对象浅拷贝(这里可以看作是C++的浅拷贝,只对目标对象赋值原对象的成员变量的值,并不复制原对象的动态分配内存,没有内存拷贝,极大地提高代码运行效率)到形参pkt指向的AVPacket对象中:

*pkt        = pktl->pkt;

让链表的头指针指向原来首结点的后一个结点。这时如果头指针为空,表示链表中除了原来的首结点外没有其它结点了,让链表的尾指针指向空:

pkt_buffer->head = pktl->next;
if (!pkt_buffer->head)
    pkt_buffer->tail = NULL;

从链表中删除原来的首结点,从而在链表的首部取出首结点对应的AVPacket数据后,可以删除掉该结点。通过av_freep函数(关于av_freep函数用法可以参考:《FFmpeg中内存分配和释放相关的源码:av_malloc函数、av_mallocz函数、av_free函数和av_freep函数分析》)释放首结点本身的空间,但av_freep函数不会释放结点成员指针指向的缓冲区,所以不用担心影响到上述通过浅拷贝得到的形参pkt指向的AVPacket对象:

av_freep(&pktl);

六、avpriv_packet_list_free函数

(一)avpriv_packet_list_free函数的声明

avpriv_packet_list_free函数声明在头文件libavcodec/packet_internal.h中:

/**
 * Wipe the list and unref all the packets in it.
 */
void avpriv_packet_list_free(PacketList *list);

该函数作用是:清空链表,删除所有结点,让所有结点中的AVPacket数据引用计数减1,只有当原本的引用计数值为1时,才会释放AVPacket结构体的成员指针指向的缓冲区。

(二)avpriv_packet_list_free函数的定义

avpriv_packet_list_free函数定义在源文件libavcodec/avpacket.c中:

void avpriv_packet_list_free(PacketList *pkt_buf)
{
    PacketListEntry *tmp = pkt_buf->head;

    while (tmp) {
        PacketListEntry *pktl = tmp;
        tmp = pktl->next;
        av_packet_unref(&pktl->pkt);
        av_freep(&pktl);
    }
    pkt_buf->head = pkt_buf->tail = NULL;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值