深耕ffmpeg系列之AVPacket

为什么要学习AVPacket?

在使用 FFmpeg 时,很多人习惯于参照示例代码来实现特定功能,却常常忽略了 FFmpeg 内部的数据管理机制。这种疏忽容易导致潜在的错误,甚至引发内存泄漏问题。AVPacket 作为 FFmpeg 中重要的数据结构,深入学习其缓冲区管理的细节以及相关 API 的实现原理,不仅能帮助我们避免常见错误,还能为熟练、高效地使用 FFmpeg 奠定坚实的基础。

AVPacket的作用

AVPacket是 FFmpeg 中用于存储编码数据的结构体,其作用主要包括以下几个方面:

  • 存储编码数据 :它主要用于存放从音视频文件中读取的或准备写入音视频文件的编码后的音频或视频数据。例如,在解复用过程中,从容器格式(如 MP4、AVI 等)中读取到的压缩的音频帧和视频帧数据会被存储在AVPacket中,以便后续进行解码等操作。
  • 数据传递 :在 FFmpeg 的各个模块之间,AVPacket充当了数据传递的载体。比如,从解复用模块到解码模块,数据就是以AVPacket的形式进行传递的。这样可以确保编码数据在不同功能模块之间能够准确、高效地传输,各个模块可以根据AVPacket中的信息对数据进行相应的处理。
  • 包含元数据 :除了存储编码数据本身,AVPacket还包含了一些关于数据的元信息。例如,它记录了数据所属的流索引,通过流索引可以知道该数据包是属于音频流还是视频流,或者是其他类型的流。此外,还包含了时间戳信息,用于标识该数据包在整个音视频流中的时间位置,这对于音视频的同步以及正确的播放顺序至关重要。
  • 支持数据复用 :在复用过程中,AVPacket用于存储准备写入到输出容器格式中的编码数据。多个不同流(音频流、视频流等)的AVPacket可以按照一定的规则被复用到一个容器中,形成完整的音视频文件。

AVPacket的定义

c 复制代码
typedef struct AVPacket {
    // 指向存储数据包数据的引用计数缓冲区的引用
    AVBufferRef *buf;
    // 以AVStream->time_base为单位的显示时间戳;
    int64_t pts;
    // 以AVStream->time_base为单位的解压缩时间戳
    int64_t dts;
    uint8_t *data;
    int   size;
    int   stream_index;
    // AV_PKT_FLAG的组合值
    int   flags;
    // container提供的附加数据
    AVPacketSideData *side_data;
    int side_data_elems;

    //当前packet的时长,以AVStream->time_base为单位, 0表示未知.
    int64_t duration;

    int64_t pos;  ///< byte position in stream, -1 if unknown

    //user使用的私有数据结构,如:在packet中传递特殊信息到后面的模块
    void *opaque;

    // AVBufferRef给API user自由使用. FFmpeg 永远不会检查其内部内容. 
    AVBufferRef *opaque_ref;

   //该数据包时间戳的时间基。
    AVRational time_base;
} AVPacket;

我们在后续章节重点理解下AVBufferRef *buf字段,其他字段在后续系列文章再分享。

AVPacket与AVBufferRef的关系

graph TD AVPacket --> AVBufferRef AVBufferRef --> AVBuffer AVBuffer --> data

我们首先来看个demo code

c 复制代码
int main() {
    // 创建一个新的 AVPacket
    AVPacket *pkt = av_packet_alloc();
    if (!pkt) {
        fprintf(stderr, "Failed to allocate packet\n");
        return -1;
    }

    // 使用 av_new_packet 分配数据
    int ret = av_new_packet(pkt, 1024);
    if (ret < 0) {
        fprintf(stderr, "Failed to allocate packet data: %s\n", av_err2str(ret));
        av_packet_free(&pkt);
        return -1;
    }
    
    av_packet_free(&pkt);
    return 0;
 }

从上面的demo可以看出av_packet_alloc()只是分配了一个AVPacket壳子,数据空间的分配是使用av_new_packet。所以压缩数据是放置在AVBufferRef中的。 下面来看下av_new_packet的实现细节:

c 复制代码
int av_new_packet(AVPacket *pkt, int size)
{
    AVBufferRef *buf = NULL;
    int ret = packet_alloc(&buf, size);
    if (ret < 0)
        return ret;

    get_packet_defaults(pkt);
    pkt->buf      = buf;
    pkt->data     = buf->data;
    pkt->size     = size;

    return 0;
}

static int packet_alloc(AVBufferRef **buf, int size)
{
    int ret;
    ret = av_buffer_realloc(buf, size + AV_INPUT_BUFFER_PADDING_SIZE);
    if (ret < 0)
        return ret;
    memset((*buf)->data + size, 0, AV_INPUT_BUFFER_PADDING_SIZE);
    return 0;
}

接下来看下av_buffer_realloc的实现:

c 复制代码
int av_buffer_realloc(AVBufferRef **pbuf, size_t size)
{
    AVBufferRef *buf = *pbuf;
    uint8_t *tmp;
    int ret;

    if (!buf) {
        uint8_t *data = av_realloc(NULL, size);

        buf = av_buffer_create(data, size, av_buffer_default_free, NULL, 0);

        buf->buffer->flags_internal |= BUFFER_FLAG_REALLOCATABLE;
        *pbuf = buf;

        return 0;
    } else if (buf->size == size)
        return 0;
    .......................................;
}

AVBufferRef *av_buffer_create(uint8_t *data, size_t size,
                              void (*free)(void *opaque, uint8_t *data),
                              void *opaque, int flags)
{
    AVBufferRef *ret;
    AVBuffer *buf = av_mallocz(sizeof(*buf)); //创建AVBuffer
    if (!buf)
        return NULL;
    //创建AVBufferRef
    ret = buffer_create(buf, data, size, free, opaque, flags);
    if (!ret) {
        av_free(buf);
        return NULL;
    }
    return ret;
}

static AVBufferRef *buffer_create(AVBuffer *buf, uint8_t *data, size_t size,
                                  void (*free)(void *opaque, uint8_t *data),
                                  void *opaque, int flags)
{
    AVBufferRef *ref = NULL;

    buf->data     = data;
    buf->size     = size;
    buf->free     = free ? free : av_buffer_default_free;
    buf->opaque   = opaque;

    atomic_init(&buf->refcount, 1);//增加引用计数

    buf->flags = flags;

    ref = av_mallocz(sizeof(*ref));
    if (!ref)
        return NULL;

    ref->buffer = buf;
    ref->data   = data;//存放数据的缓存空间
    ref->size   = size;

    return ref;
}

AVBufferRef的引用计数

c 复制代码
#include <stdio.h>
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>

int main() {
    // 分配一个新的 AVPacket
    AVPacket *src_pkt = av_packet_alloc();

    // 此时AVBuffer的引用计数为1
    int ret = av_new_packet(src_pkt, 1024);

    // 分配一个新的 AVPacket 用于引用
    AVPacket *dst_pkt = av_packet_alloc();

    // 此时AVBuffer的引用计数为2
    ret = av_packet_ref(dst_pkt, src_pkt);

    //  此时AVBuffer的引用计数为1
    av_packet_unref(dst_pkt);

    // 释放资源,释放AVBuffer
    av_packet_free(&src_pkt);
    av_packet_free(&dst_pkt);

    return 0;
}

需要注意的是av_packet_ref只是会将AVPacket内部的字段设置为默认值,AVPacket内部的AVBufferRef字段会被free并设置为NULL,但最后还是需要调用av_packet_free,不然有内存泄漏。下面是一个典型的应用场景:

c 复制代码
// 分配 AVPacket
    packet = av_packet_alloc();
    if (!packet) {
        fprintf(stderr, "Could not allocate packet\n");
        return -1;
    }

    // 循环使用packet这个外壳
    while (av_read_frame(format_context, packet) >= 0) {
         // 针对packet做一些处理

        // 释放当前数据包的引用
        av_packet_unref(packet);
    }

    // 释放资源
    av_packet_free(&packet);

AVPacket API总结

重点函数 作用
AVPacket *av_packet_alloc(void) 分配一个AVPacket地址空间
void av_packet_free(AVPacket **pkt) 先unref将引用计数减一,free AVBufferRef ,如果引用计数为0,则free AVBuffer
int av_new_packet(AVPacket *pkt, int size) 为pkt分配AVBufferRef以及AVBuffer
int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size) 相当于av_packet_alloc + av_new_packet,并将数据data拷贝到AVBuffer中
int av_packet_ref(AVPacket *dst, const AVPacket *src) 如果src是可以被引用的,则dst直接引用src中的AVBuffer,如果src不能被引用,则new一个AVBuffer给dst,并拷贝src的数据
void av_packet_unref(AVPacket *pkt) 减少引用计数并free AVBufferRef,并将pkt内部字段全部设置到默认值,如果引用计数为0,则free AVBuffer
AVPacket *av_packet_clone(const AVPacket *src) 相当于 av_packet_alloc()+av_packet_ref(),clone之后的AVPacket与src共同引用一个AVBuffer
void av_packet_move_ref(AVPacket *dst, AVPacket *src) 移动src的所有字段到dst,并reset src,计src编程一个空壳,内部的字段全部是默认值
void av_shrink_packet(AVPacket *pkt, int size) 缩减packet buffer size,例如,你只想要packet中前100个字节的数据继续处理,则可以av_shrink_packet(AVPacket *pkt, 100)
int av_grow_packet(AVPacket *pkt, int grow_by) 为PKT扩充grow_by个字节的大小
相关推荐
晓龙的Coding之路33 分钟前
python生成项目依赖文件requirements.txt
linux·开发语言·python
gblfy43 分钟前
DeepSeek + Dify + Ollama + Docker + Linux 私有化部署,构建你的专属私人 AI 助手
linux·docker·dify·本地部署·ollama·deepseek·私有化
ℳℓ白ℳℓ夜ℳℓ1 小时前
Linux网络UDP与TCP
linux·网络·udp
小oo呆1 小时前
【自然语言处理与大模型】Linux环境下Ollama下载太慢了该怎么处理?
linux·服务器·人工智能
菜鸡上道1 小时前
Linux 文件系统目录结构详解
linux
eli9602 小时前
LIB-ZC, 一个跨平台(Linux)平台通用C/C++扩展库, 网络socket
linux·c语言·c++
破刺不会编程2 小时前
关于进程状态
linux·服务器
632972 小时前
Linux守护进程
linux·运维·服务器
Yusei_05232 小时前
Linux 进程概念补充 (自用)
linux·运维·服务器
u0109362652 小时前
Linux电源管理(三),CPUIdle 和 ARM的PSCI
linux