FFmpeg之AVPacket

2,589 阅读5分钟

在FFmpeg中,AVPacket主要存储编码数据,例如:H264、H265、AAC等。对于视频流,它通常应该包含一个编码帧;对于音频流,则可能包含多个音频帧。编码器可能输出空AVPacket,不包含编码数据,只包含边side data,例如:在编码结束时更新一些流参数。

AVPacket结构体如下所示:

typedef struct AVPacket {
    /**
     * 若为空,则表示AVPacket未使用引用计数管理负载数据,否则指向存储负载数据的引用计数AVBufferRef -> AVBuffer
     * A reference to the reference-counted buffer where the packet data is
     * stored.
     * May be NULL, then the packet data is not reference-counted.
     */
    AVBufferRef *buf;
    /**
     * 基于AVStream->time_base的pts,若文件中没有,则为AV_NOPTS_VALUE。
     * pts必须大于等于dts,因为显示不能早于解码
     */
    int64_t pts;
    /**
     * 基于AVStream->time_base的dts,若文件中没有,则为AV_NOPTS_VALUE。
     */
    int64_t dts;
    // 负载数据
    uint8_t *data;
    // 负载数据的长度
    int   size;
    // 属于AVFormatContext中的哪个AVStream
    int   stream_index;
    /**
     * A combination of AV_PKT_FLAG values,AV_PKT_FLAG_KEY表示关键帧
     */
    int   flags;
    /**
     * Additional packet data that can be provided by the container.
     * Packet can contain several types of side information.
     * 携带的不同类型的side data
     */
    AVPacketSideData *side_data;
    int side_data_elems;

    /**
     * Duration of this packet in AVStream->time_base units, 0 if unknown.
     * Equals next_pts - this_pts in presentation order.
     * 当前帧的持续时间,基于AVStream->time_base
     */
    int64_t duration;
    // byte position in stream, -1 if unknown
    int64_t pos;                            
} AVPacket;

几个关键点:

  1. AVPacket的时间戳都是基于AVStream->time_base时间基准。
  2. dts是解码时间,pts是显示时间,pts必须大于等于dts,因为只有先解码,才能显示。
  3. AVPacket->buf决定了负载数据的管理方式,若为空,则表示AVPacket未使用引用计数管理负载数据,AVPacket->data就是负载数据;否则,则使用引用计数管理负载数据,AVPacket->buf引用AVBufferRef,AVBufferRef引用AVBuffer,AVBuffer存储了负载数据,关于引用计数下面👇详细介绍。

side_data和side_data_elems

AVPacket->side_data是AVPacket携带的side数据数组,AVPacket->side_data_elems是数组的长度。av_packet_new_side_dataav_packet_add_side_data函数都提供了向AVPacket添加指定类型side data的能力,只是参数略有差异,每次都会把新side data添加到数组的尾部。 av_packet_get_side_data函数提供了从AVPacket获取指定类型side data的能力。

typedef struct AVPacketSideData {
    uint8_t *data;
    int      size;
    // side data的类型
    enum AVPacketSideDataType type;
} AVPacketSideData;

针对AVPacket的函数

FFmpeg提供了很多函数操作AVPacket,其中主要需要关注的是不同函数对AVPacket引用计数的影响。首先看下与引用计数相关的AVBufferRef和AVBuffer结构体:

typedef struct AVBufferRef {
    // 
    AVBuffer *buffer;

    /**
     * The data buffer. It is considered writable if and only if
     * this is the only reference to the buffer, in which case
     * av_buffer_is_writable() returns 1.
     */
    uint8_t *data;
    /**
     * Size of data in bytes.
     */
    int      size;
} AVBufferRef;

struct AVBuffer {
    // 具体数据和Size
    uint8_t *data; /**< data described by this buffer */
    int      size; /**< size of data in bytes */

    /**
     * 引用当前AVBuffer的AVBufferRef实例数,即引用计数
     * number of existing AVBufferRef instances referring to this buffer
     */
    atomic_uint refcount;

    /**
     * a callback for freeing the data,释放AVBuffer时,会通过该接口释放data数据
     */
    void (*free)(void *opaque, uint8_t *data);

    /**
     * an opaque pointer, to be used by the freeing callback,调用free函数的参数
     */
    void *opaque;

    /**
     * A combination of BUFFER_FLAG_*
     */
    int flags;
};

AVBufferRef引用AVBuffer,每增加一个指向同一个AVBuffer的AVBufferRef,AVBuffer中的引用计数计数就加1;相反,每减少一个指向AVBuffer的AVBufferRef,AVBuffer中的引用计数就减1,当引用计数等于0时,就会释放AVBuffer.data以及AVBuffer本身,类似于C++的智能指针。 AVPacket引用计数

av_packet_alloc

分配并返回AVPacket,所有参数都是默认值,此时并没有分配AVPacket->buf,因为AVPacket还没有包含有效负载数据。

AVPacket *av_packet_alloc(void)
{
    AVPacket *pkt = av_mallocz(sizeof(AVPacket));
    if (!pkt)
        return pkt;
        
    // 减少pkt指向的AVBuffer的引用计数,若计数等于0则释放AVBuffer。同时,释放side_data,其他参数则reset默认值。
    av_packet_unref(pkt);
    return pkt;
}

av_packet_free

释放AVPacket自身内存和AVBufferRef自身内存,并且减少AVBufferRef指向的AVBuffer的引用计数,若计数等于0则释放AVBuffer。

void av_packet_free(AVPacket **pkt)
{
    if (!pkt || !*pkt)
        return;
        
     // 减少pkt指向的AVBuffer的引用计数,若计数等于0则释放AVBuffer。同时,释放side_data,其他参数则reset默认值。
    av_packet_unref(*pkt);
    av_freep(pkt);
}

av_new_packet

为已有的AVPacket分配负载数据,设置引用计数,并且通过av_init_packet函数把其他参数设置为默认值。

int av_new_packet(AVPacket *pkt, int size)
{
    AVBufferRef *buf = NULL;
    // 分配负载数据,设置引用计数
    int ret = packet_alloc(&buf, size);
    if (ret < 0)
        return ret;
    
    // 设置默认值
    av_init_packet(pkt);
    // 赋值引用计数AVBufferRef和负载数据
    pkt->buf      = buf;
    pkt->data     = buf->data;
    pkt->size     = size;

    return 0;
}

av_init_packet

把AVPacket的字段设置初始化为默认值,但是并不会为AVPacket->data和AVPacket->size设置值,因为此时还没有包含负载数据。

void av_init_packet(AVPacket *pkt)
{
    pkt->pts                  = AV_NOPTS_VALUE;
    pkt->dts                  = AV_NOPTS_VALUE;
    pkt->pos                  = -1;
    pkt->duration             = 0;
#if FF_API_CONVERGENCE_DURATION
FF_DISABLE_DEPRECATION_WARNINGS
    pkt->convergence_duration = 0;
FF_ENABLE_DEPRECATION_WARNINGS
#endif
    pkt->flags                = 0;
    pkt->stream_index         = 0;
    pkt->buf                  = NULL;
    pkt->side_data            = NULL;
    pkt->side_data_elems      = 0;
}

av_packet_ref

把src的数据copy到dst,包含两部分:

  1. 普通字段直接通过av_packet_copy_props函数copy,例如:side data、pts等
  2. 负载数据:为dst AVPacket分配AVBufferRef,指向src的AVBuffer,两个AVPacket共享负载数据AVBuffer,增加了AVBuffer的引用计数。
int av_packet_ref(AVPacket *dst, const AVPacket *src)
{
    int ret;
    // 把src的参数copy到dst,不包含负载数据
    ret = av_packet_copy_props(dst, src);
    if (ret < 0)
        return ret;

    if (!src->buf) { // 若src不是引用计数,则为dst创建AVBufferRef,初始化引用计数为1,并且把src的负载数据(AVPacket->data)copy到AVBuffer中
        ret = packet_alloc(&dst->buf, src->size);
        if (ret < 0)
            goto fail;
        // copy有效负载数据    
        if (src->size)
            memcpy(dst->buf->data, src->data, src->size);

        dst->data = dst->buf->data;
    } else { // 若src已经是引用计数,则基于src->buf创建并返回AVBufferRef,并增加AVBuffer的引用计数
        dst->buf = av_buffer_ref(src->buf);
        if (!dst->buf) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        dst->data = src->data;
    }

    dst->size = src->size;

    return 0;
fail:
    av_packet_free_side_data(dst);
    return ret;
}

av_packet_unref

把AVPacket的普通变量设置为默认值,释放所有的side data,并且,释放AVBufferRef自身内存,减少AVBufferRef指向的AVBuffer的引用计数,若引用计数等于0,则释放AVBuffer

void av_packet_unref(AVPacket *pkt)
{
    // 删除所有的side data
    av_packet_free_side_data(pkt);
    // 释放AVBufferRef自身内存,并且减少AVBufferRef指向的AVBuffer的引用计数,若引用计数等于0,则释放AVBuffer
    av_buffer_unref(&pkt->buf);
    // 把AVPacket的其他变量设置为默认值
    av_init_packet(pkt);
    pkt->data = NULL;
    pkt->size = 0;
}

av_packet_move_ref

把scr的所有字段move到dst,并把src的字段重置为默认值,此时AVPacket对AVBuffer的引用计数并不会变。

void av_packet_move_ref(AVPacket *dst, AVPacket *src)
{
    *dst = *src;
    // 重置src AVPacket的所有字段
    av_init_packet(src);
    src->data = NULL;
    src->size = 0;
}

av_packet_clone

首先通过av_packet_alloc创建一个AVPacket,然后使用av_packet_ref把src AVPacket copy到新创建的AVPacket。

AVPacket *av_packet_clone(const AVPacket *src)
{
    AVPacket *ret = av_packet_alloc();
    if (!ret)
        return ret;
    if (av_packet_ref(ret, src))
        av_packet_free(&ret);
    return ret;
}

总结

AVPacket的字段包含两部分:

  1. 普通字段:除AVPacket->data、AVPacket->size和AVPacket->buf之外的所有字段,这些字段依附于AVPacket自身,通过av_init_packet重置为默认值。
  2. 负载数据:AVPacket->data、AVPacket->size和AVPacket->buf,负载数据需要单独创建(av_new_packet),通过引用计数进行管理。

所有关于AVPacket的函数都是针对这两部分操作:

  • av_packet_alloc与av_packet_free相对应:前者分配AVPacket自身内存,后者释放AVPacket自身内存,并且通过av_packet_unref释放AVBufferRef自身内存,减少AVBuffer引用计数。
  • av_packet_ref:把src AVPacket的所有字段copy到dst AVPacket,并且为dst AVPacket创建AVBufferRef,指向同一个AVBuffer。最终,两个AVPacket指向同一个AVBuffer负载数据,引用计数也会加1。
  • av_packet_unref:释放AVPacket的AVBufferRef,并且减少AVBuffer的引用计数,同时把其他字段设置为默认值。
  • av_packet_move_ref:av_packet_ref是copy操作,最终两个AVPacket都有效,引用计数增1;而av_packet_move_ref是move操作,最终src AVPacket废弃,dst AVPacket有效,引用计数不变。
  • av_packet_clone:相当于av_packet_alloc + av_packet_ref