FFmpeg 的 AVPacket几乎所有做过音视频开发的人都在代码里见过它但真正把它用明白的人不多。很多人写了半天av_packet_alloc、avcodec_send_packet遇到内存泄漏、pts/dts 错乱、音画不同步就一头雾水。这篇文章就专门聊透 AVPacket它的结构、生命周期、在编解码管线里的流转方式以及我这些年用它踩过的坑。适合刚接触 FFmpeg 的初学者也适合已经写过一段时间 FFmpeg 代码但始终没彻底搞懂 packet 内存语义的开发者。1. 认识 AVPacket音视频数据的“快递包裹”1.1 为什么必须理解 AVPacketFFmpeg 的整个音视频处理管线本质上就是一堆数据在不同模块之间流转。解复用器从文件或网络流里读出来的是封装格式的数据编码器吃进去的是原始图像或 PCM 数据输出的是压缩码流。而这些“数据块”在模块之间传递时不能光给一块裸内存还得带上类型、时间戳、流索引等信息统一装进一个结构体里这个结构体就是 AVPacket。你可以把 AVPacket 理解成一个快递包裹data是包裹里的货物size是货物大小pts/dts是包裹的物流时间标签stream_index是它该进哪条流水线flags是包裹的加急或配件标记。没有这个统一的包裹形态没人能搞清手里这块内存到底是什么数据、该交给谁、什么时候该销毁。FFmpeg 用 AVPacket 把“数据本身”和“数据的描述信息”绑定在一起才是整个编解码管线能顺畅跑起来的基础。1.2 AVPacket 与 AVFrame 的分工很多人刚开始搞混 AVPacket 和 AVFrame。一句话区分AVPacket 装的是压缩后的码流数据AVFrame 装的是解码后或编码前的原始数据。解码场景里解复用器给解码器的是 AVPacket解码器吐出来的是 AVFrame编码场景里编码器吃进去的是 AVFrame输出的是 AVPacket。这个区别直接影响内存管理的思维。压缩码流比如 H.264 的 NAL 单元通常是一段连续内存结构上是字节流而 AVFrame 里的图像数据可能包含多行、多通道、对齐填充像素格式不同内存布局千变万化。FFmpeg 对 AVPacket 和 AVFrame 的内存管理策略也因此不同AVPacket 在生命周期上更轻量但使用粗暴了内存问题照样一大堆。理解二者分工后续处理时间戳、缓冲区和对齐方式时就不容易糊涂。2. AVPacket 核心结构逐字段拆解2.1 关键字段与语义AVPacket 定义在 FFmpeg 公共头文件里结构不算复杂但每个字段背后都有讲究。我用一个表格把最常用的字段和注意事项列出来。字段类型含义注意事项datauint8_t*指向压缩码流数据缓冲区可能含填充字节AV_INPUT_BUFFER_PADDING_SIZE解码时不能只看sizesizeint缓冲区有效数据大小不等于buf-sizebuf里可能有额外空间ptsint64_t显示时间戳单位取决于对应流的time_base不是秒也不是微秒dtsint64_t解码时间戳某些封装格式没有 dts可能为AV_NOPTS_VALUEdurationint64_t该 packet 媒体的时长单位也是time_base不是所有格式都有stream_indexint属于哪条流从av_read_frame拿到的 packet 必须用它索引流flagsint标志位关键帧、损坏帧等用AV_PKT_FLAG_KEY判断关键帧不要直接 scannerdata[4]之类的字节判 I 帧side_dataAVPacketSideData*附加数据如 H.264 的 SPS/PPS、加密信息不是所有信息都在data里部分关键信息藏在这里side_data_elemsintside_data 元素个数遍历时要使用该计数bufAVBufferRef*引用计数的底层缓冲区没有它时data指向的是外部内存posint64_t在输入文件中的字节位置有些格式拿不到不用依赖time_baseAVRational当前 packet 的时间基较新版旧版本没有需要从流里取opaque/opaque_ref用户自定义数据没有它时data指向的是外部内存opaque_ref可带走引用用于贯穿解码过程传上下文这些字段里最容易被忽略的是data与buf的关系。AVPacket本身是一个“容器的壳”其data指针可能有两种来源一是 FFmpeg 内部通过av_packet_alloc后由解复用器填充buf字段指向一个引用计数缓冲区二是你自己手动把data指到外部的一块内存。后一种情况很危险因为av_packet_unref只释放buf引用的内存如果buf为空它不会释放你的外部内存但也正因为如此不会误释放前提是你知道自己负责管理那块内存。2.2 pts、dts 和时间基的换算逻辑ptspresentation timestamp是“什么时候把这一帧显示给用户”dtsdecode timestamp是“什么时候解码这一帧”。对于大部分没有 B 帧的编码格式pts 和 dts 相同一旦有 B 帧dts 小于 pts解码器必须先解码 B 帧的参考帧dts 才体现出真实的解码顺序。处理 pts/dts 时最大的坑是单位。AVPacket里的pts不是毫秒不是微秒也不是秒而是对应流time_base的整数倍。比如视频流time_base是1/90000一个 pts 值 3600 实际代表 3600/90000 0.04 秒。转换到毫秒时不能写pts / 90而是要用av_rescale_q这类接口。pts_ms av_rescale_q(pkt-pts, stream-time_base, (AVRational){1, 1000});反向操作同理把播放器的毫秒时间戳塞回 packet 前要先换算成流的time_base才行。很多音画不同步问题就是这里出了偏差。另一个常见问题是AV_NOPTS_VALUE。某些封装格式比如裸的 ES 流或部分 TS 流没有完整的时间信息dts可能是AV_NOPTS_VALUE(int64_t)0x8000000000000000。如果直接把这种值往其他接口里传会出现诡异的计算结果。稳妥做法是先判断再根据pts或上一帧的dtsduration做推断。3. 生命周期管理从分配、引用到释放3.1 最基础的三件套FFmpeg 提供了三个最基础的生命周期接口所有 AVPacket 操作都围绕它们展开av_packet_alloc()分配一个空的 AVPacket 结构体只分配外壳不分配数据缓冲区。返回值用av_packet_free()释放。av_packet_free()把 AVPacket 内部引用的数据缓冲区一并释放并把传入的指针置空避免悬空指针。av_packet_unref()只消减引用计数不释放 AVPacket 结构体本身包可以被再次复用。这三者的差异几乎就是内存管理出错的重灾区。很多人写完av_read_frame循环后只调用av_packet_free没有在每次循环里av_packet_unref导致了严重的内存增长。因为av_read_frame每次返回的新 packet 都会重新分配数据缓冲你不 unref之前的缓冲区就一直被占着。正确流程应该是AVPacket *pkt av_packet_alloc(); while (av_read_frame(fmt_ctx, pkt) 0) { // 处理 pkt解码 / 分析 / 转封装 ... av_packet_unref(pkt); // 释放本次读取的缓冲区pkt 本身留待复用 } av_packet_free(pkt);这正是 AVPacket 设计的核心特点外壳重用数据缓冲随取随放。合理复用同一个 AVPacket 对象能显著降低频繁分配结构体带来的开销。3.2 引用计数与浅拷贝AVPacket 的拷贝语义是另一个高频造坑点。直接*new_pkt *pkt是浅拷贝两个 AVPacket 的data指向同一块缓冲区。此时如果对任意一个调用av_packet_unref缓冲区引用计数归零另一个指针就变成悬空指针再用就崩溃。要安全地复制 packet 内容必须用av_packet_refAVPacket *dst av_packet_alloc(); if (av_packet_ref(dst, src) 0) { // 处理错误 }av_packet_ref会做两件事一是将src中的buf引用计数加一二是把data、size、pts、dts等字段值拷贝到dst。此时两个 AVPacket 共享同一份底层数据内存但各自有独立外壳任何一方 unref 都不会影响另一方安全很多。底层内存要等到最后一个引用被 unref 时才真正释放。理解这种类似于“智能指针”的语义对编写长时间运行的程序尤其重要。你可能会把同一个 packet 同时交给多个下游消费者比如解码器和某种数据统计模块这时不要多次浅拷贝而是让所有消费者各自持有一个AVPacketRef。av_packet_move_ref也是常用的接口。它把src的引用“转移”给dst转移完成后src变为空包data为空、buf为空不需要对src再调用 unref。相当于std::move语义清爽适合管道式传递。我用它做 packet 队列时最多。3.3 缓冲区复用与手动包装某些场景下你需要绕过av_read_frame手动把一个内存块包装成 AVPacket。比如从自定义协议解析出的 H.264 分片想直接塞给解码器AVPacket pkt; av_init_packet(pkt); // 旧版本才需要新版本 av_packet_alloc 已初始化 pkt.data raw_data; pkt.size raw_size; pkt.stream_index 0; pkt.pts pkt.dts current_pts;这种情况下pkt.buf为空FFmpeg 解码器读取数据时不会尝试释放data因为该内存归你所有。但要注意解码器如avcodec_send_packet可能不会立即拷贝数据它会在解码过程中引用 packet 内容。所以必须保证这个手工包装的 packet 在avcodec_send_packet返回之后、解码器真正使用数据期间仍然有效。最简单的稳妥做法是调用av_packet_make_refcounted让 FFmpeg 把数据复制到自己的引用计数缓冲区这样 raw 内存的存活问题就交给 FFmpeg 了av_packet_make_refcounted(pkt);之后你就可以安全地释放原始的内存块FFmpeg 内部会管理数据。这个接口在实际工程里非常实用但很多教程没提过。4. 实操示例从解复用到解码的完整流转4.1 读取 packet 与基本判断完整的流程是建立解码器上下文AVCodecContext、打开输入文件之后进入一个读取循环。这里以一个常见的.mp4文件解码为例AVFormatContext *fmt_ctx NULL; AVPacket *pkt av_packet_alloc(); if (avformat_open_input(fmt_ctx, filename, NULL, NULL) 0) { return -1; } if (avformat_find_stream_info(fmt_ctx, NULL) 0) { return -1; } // 找到视频流和对应的解码器上下文 int video_stream_index -1; AVCodecContext *dec_ctx NULL; for (unsigned i 0; i fmt_ctx-nb_streams; i) { if (fmt_ctx-streams[i]-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { video_stream_index i; const AVCodec *dec avcodec_find_decoder(fmt_ctx-streams[i]-codecpar-codec_id); dec_ctx avcodec_alloc_context3(dec); avcodec_parameters_to_context(dec_ctx, fmt_ctx-streams[i]-codecpar); avcodec_open2(dec_ctx, dec, NULL); break; } } AVFrame *frame av_frame_alloc(); while (av_read_frame(fmt_ctx, pkt) 0) { if (pkt-stream_index video_stream_index) { int ret avcodec_send_packet(dec_ctx, pkt); if (ret 0 ret ! AVERROR(EAGAIN) ret ! AVERROR_EOF) { // 解码错误处理 } while (ret 0) { ret avcodec_receive_frame(dec_ctx, frame); if (ret AVERROR(EAGAIN) || ret AVERROR_EOF) { break; } else if (ret 0) { break; } // 此时 frame 里就是解码后的图像 process_decoded_frame(frame, fmt_ctx-streams[video_stream_index]-time_base); av_frame_unref(frame); } } av_packet_unref(pkt); } av_frame_free(frame); avcodec_free_context(dec_ctx); avformat_close_input(fmt_ctx); av_packet_free(pkt);这是我实际操作中总结的技巧在把 packet 送入解码器之前先判断stream_index避免把音频数据发送给视频解码器否则视频解码器会报类型不匹配。解码时要用avcodec_send_packetavcodec_receive_frame这套现代 API而不是旧的avcodec_decode_video2因为前者支持一次发多个 packet 后多次receive取出多个帧更适合 B 帧和 frame threading。4.2 time_base 的正确传递与转换上面的process_decoded_frame接收AVFrame后很多人习惯直接拿frame-pts来显示或保存。实际上AVFrame的pts继承自原始AVPacket的pts前提是解码器内部没有重新赋值单位同样是流的时间基不是任何全局时间轴上的数值。如果要对 frame 做显示或者写入输出文件就需要确定统一的时间基。比如输出到 MP4 时约定写入容器的时间基通常是流的时间基或1/90000。官方推荐的方式是使用av_rescale_q做精确换算AVRational src_time_base fmt_ctx-streams[video_stream_index]-time_base; AVRational dst_time_base (AVRational){1, 90000}; int64_t out_pts av_rescale_q(frame-pts, src_time_base, dst_time_base);不要自己写int64_t pts_ms frame-pts * 1000 * src_time_base.num / src_time_base.den;虽然也能算但括号、溢出、负数的问题防不胜防。av_rescale_q内部已经处理好越界和舍入简单可靠。还有一个细节如果解码器配置了AV_CODEC_FLAG_OUTPUT_CORRUPT之类的扩展标志frame-pts可能不再等同于 packet 的 pts这种情况很少见但一旦出现排查起来很费时间。遇到不合理的 pts 跳变建议先打印一两个 packet 的原始 pts 做对照。4.3 分组、缓存与转封装时的 packet 处理在需要把解码后的帧重新编码或直接转封装remux的场景里packet 处理更讲究。转封装时你要保持 packet 的时间戳和原始顺序不变只改变封装格式。这时要注意的是从av_read_frame得到 packet 后不能只改stream_index就塞给 muxer还需要根据输出流的time_base转换时间戳。若输入和输出 time_base 不一致不转换会导致音画不同步或播放器时间轴错乱。转封装最好使用av_packet_rescale_ts来统一换算它同时处理 pts、dts、durationav_packet_rescale_ts(pkt, in_stream-time_base, out_stream-time_base); pkt-stream_index out_stream_index;如果不需要修改 packet 内容且原 packet 非引用计数缓冲区比如解码器重新生成的编码包通常是引用计数的没问题转封装前应保证pkt-buf非空否则某些 muxer 会拒绝接收或者复制失败。必要时用av_packet_make_refcounted补救。编码场景则相反从avcodec_receive_frame得到原始帧送入编码器avcodec_receive_packet拿到编码后的 packet这时的 packet 通常是引用计数缓冲可以直接送入 muxer。但要注意编码器输出的 packet 的pts可能需要基于编码器的time_base再转换成输出容器的time_base不要偷懒跳过。5. 常见问题与排查技巧实录5.1 内存持续增长这是我在帮人调试 FFmpeg 相关程序时遇到最多的一个问题。症状表现为程序跑的时间越长占内存越大典型的代码往往是循环里漏掉了av_packet_unref(pkt)或者av_frame_unref(frame)之后继续使用之前的字段。FFmpeg 的解复用器和编码器内部都有缓冲你不主动 unref旧数据就永远不会被归还。排查方法在进程内每隔一段时间打印一次pkt-buf的引用计数或进程 RSS如果 RSS 线性增长先检查循环末尾是否调用了 unref。也可以开valgrind跑一个短样本直接定位未释放的内存块来源。这里补充一个容易忽略的点avcodec_send_packet在内部可能引用 packet 数据因此如果送完包马上 unref解码器内部还在用这种情况不会崩溃因为 FFmpeg 内部持有引用计数但如果你用的是非引用计数包装的AVPacket就会发生“解码器仍在读取已被释放内存”的野指针问题。所以手动包装的 packet 一定要先av_packet_make_refcounted。5.2 pts/dts 异常负值、跳变、AV_NOPTS_VALUEpts/dts 出现负值常见原因是从流中间开始读取时时间戳没有重新基线化。例如网络流或重播文件dts可能从某个负偏差开始。处理方式是记录第一帧的dts后续每个值减去它。跳变则多半源于转封装或编码时没有正确统一time_base。我遇到过某个工程里输入流 time_base 是1/15360输出流 time_base 是1/90000由于直接用整型乘法做转换在长视频上误差累积到一小时之后音画不同步了 2 秒。换成av_rescale_q后立刻稳定。AV_NOPTS_VALUE是另一个坑。判断它必须用if (pkt-dts ! AV_NOPTS_VALUE)不能判断pkt-dts 0因为AV_NOPTS_VALUE是负的INT64_MIN附近的一个极大负数。曾经有同事写成if (pkt-dts 0)结果把所有合法负 dts 都过滤掉了。用官方宏判断错不了。5.3 packet 缓冲区和 padding 的关系data指向的缓冲区末尾通常有AV_INPUT_BUFFER_PADDING_SIZE64 字节的额外填充。这是为了兼容一些解码器做位流解析时的越界读取。直接memcpy(pkt-data, buf, pkt-size)到一个小缓冲区再去解码往往报错或崩溃正确做法是拷贝到size AV_INPUT_BUFFER_PADDING_SIZE的缓冲区并把 padding 区域清零。FFmpeg 自己从文件读取的 packet 已经带上 padding不需要你额外处理但如果你的数据来自自定义读取器必须自行补 padding。这一条很多人不知道却经常是诡异解码崩溃的根源。size和底层缓冲区长度不是一回事。引用计数的buf-size可能大于pkt-size这只是缓冲区的容量解码时只应读取pkt-size范围。如果手动改动pkt-data如去掉头部同时也要更新size否则解码器会多吃到无用的字节。5.4 side_data 丢失与关键帧判断某些流的关键参数放在 side data 里。例如 H.264 码流中SPS/PPS 有可能在普通 packet 里也有可能作为AV_PKT_DATA_NEW_EXTRADATAside data 附加在第一个关键帧包上。当你转而使用裸流如 annex-b 格式时从codecpar-extradata拿不到完整参数就得从 packet 的side_data中提取。如果编码器在关键帧前没有把参数刷出来播放端可能黑屏。因此不要用“data[4] 0x1f 7”这种字节扫描方式判断 H.264 是否关键帧直接检查pkt-flags AV_PKT_FLAG_KEY。对于 H.265字节扫描更不靠谱。用官方标志位判断最简单也最稳。5.5 AVPacket 放进队列时的深拷贝问题做实时音视频处理时经常需要把 AVpacket 放入线程间的队列。这时如果直接浅拷贝AVPacket结构体再原包 unref队列里的包就废了。我早期吃过这个亏后来统一改为入队时av_packet_ref并复制结构体出队时处理完av_packet_unref。如果要实现深拷贝还有一个技巧是手动创建一个新的引用计数缓冲区AVPacket *deep_copy_packet(const AVPacket *src) { AVPacket *p av_packet_alloc(); av_packet_copy_props(p, src); av_new_packet(p, src-size); memcpy(p-data, src-data, src-size); return p; }av_packet_copy_props会把pts、dts、side_data、flags等全部元数据拷贝过来已经很完善不需要自己逐个赋值。但注意av_packet_copy_props不会拷贝data/size需要配合av_new_packet分配新缓冲区。这种深拷贝方式只应在确有必要时使用如果只是队列传递消费职责av_packet_ref的引用计数方式更高效因为底层数据不用复制。6. 关于 AVPacket 的几条独家经验很多文档都在讲 API 用法我这里补充几条来自真实项目的经验不一定写在官方手册里但实际排查问题特别有用。第一条所有 packet 操作都要放在与解码器同一套错误处理框架里。FFmpeg 的avcodec_send_packet返回值有三种常见情况成功0、AVERROR(EAGAIN)解码器内部缓冲满需要先 receive、AVERROR_EOF解码器已刷新。不要把EAGAIN当作错误直接退出解码循环。正确处理方式是收到EAGAIN后继续调用avcodec_receive_frame直到资源释放再重新调用send_packet否则会出现丢帧。第二条avcodec_send_packet传入以 NULL 替代的 packet 时表示刷新解码器缓冲这时要配合多次循环调用avcodec_receive_frame把尾帧取干净。拿不到这个细节编码后的视频经常会少最后一帧或者音频末尾出现截断。刷新时我通常先 flush 之后再做一次avcodec_receive_frame循环直到返回AVERROR_EOF才停止。第三条多路流场景下不要以为av_read_frame返回的 packet 顺序是按显示顺序排列的。文件中的 packet 顺序通常是按 dts 排列的。如果业务上要求按播放顺序处理必须另外做重排。某些 MP4 文件内部的 packet 顺序与pts不一致如果你直接把 packet 顺序和pts强绑定音画同步就会出问题。遇到这类情况先打印一批dts/pts看看分布再做决策。最后一条AVPacket 的调试很有技巧先用 FFmpeg 自带的工具打印检测信息对比自己程序输出的时间戳往往一个字节一个字节地对照.h264/.aac裸流就能定位错误出在封装、解码还是时间戳阶段。调试时我会在关键点位打印pts、dts、size、stream_index、flags五个字段形成流水日志比用 debugger 单步跟踪快得多。如果发现 size 变 0 但 data 非空多半是 unref 过多导致的悬空如果 pts 突然跳到负值检查是否错误地使用了int类型保存int64_t。这些都是看起来不起眼但非常致命的细节。FFmpeg 的 AVPacket 说难不难说简单也有一堆隐藏语义。把它彻底搞清楚写解码、编码、转封装、直播推流的代码都会顺畅很多。希望这篇拆解能帮你少走点弯路。