解封装
常用函数
1. avformat_open_input()
作用
- 打开媒体文件或网络资源:解析文件路径或 URL,识别媒体格式(如 MP4、AVI、RTSP 等)。
- 初始化
AVFormatContext
:分配并初始化AVFormatContext
结构体,用于存储媒体文件的元数据和流信息。 - 准备后续操作:为后续的解封装(demuxing)和解码操作做好准备。
典型用法
AVFormatContext* fmt_ctx = NULL;
if (avformat_open_input(&fmt_ctx, input_file, NULL, NULL) < 0) {fprintf(stderr, "Could not open input file: %s\n", input_file);return -1;
}
关键点
- 输入参数:
AVFormatContext** fmt_ctx
:指向AVFormatContext
指针的指针,用于存储媒体文件的上下文。const char* url
:文件路径或 URL。AVInputFormat* fmt
:指定输入格式(通常为NULL
,自动检测)。AVDictionary** options
:额外的选项(如超时、缓冲区大小等)。
- 输出:
- 成功时返回
0
,失败时返回负值。 - 初始化后的
AVFormatContext
包含媒体文件的元数据和流信息。
- 成功时返回
2. avformat_find_stream_info()
作用
- 解析流信息:分析媒体文件中的视频、音频流,提取编码器类型、帧率、时长、分辨率等关键信息。
- 填充
AVFormatContext
:将解析到的流信息填充到AVFormatContext
的streams
数组中。 - 准备解码:为后续的解码操作分配必要的缓冲区和数据结构。
典型用法
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {fprintf(stderr, "Could not find stream information\n");avformat_close_input(&fmt_ctx);return -1;
}
关键点
- 输入参数:
AVFormatContext* fmt_ctx
:已打开的AVFormatContext
。AVDictionary** options
:额外的选项(如最大读取时长、最大帧数等)。
- 输出:
- 成功时返回
0
,失败时返回负值。 fmt_ctx->streams
数组包含所有流的信息(如视频、音频、字幕等)。
- 成功时返回
3. av_find_best_stream()
作用
- 查找最佳流:根据指定的流类型(如视频、音频)在
AVFormatContext
中查找最佳匹配的流。 - 简化流选择:避免手动遍历所有流,自动选择最合适的流。
典型用法
int video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (video_stream_idx < 0) {fprintf(stderr, "Could not find video stream\n");avformat_close_input(&fmt_ctx);return -1;
}
关键点
- 输入参数:
AVFormatContext* fmt_ctx
:已打开的AVFormatContext
。enum AVMediaType type
:流类型(如AVMEDIA_TYPE_VIDEO
、AVMEDIA_TYPE_AUDIO
)。int wanted_stream_nb
:期望的流索引(通常为-1
,自动选择)。int related_stream
:相关流索引(通常为-1
)。AVCodec** decoder_ret
:返回解码器(通常为NULL
)。int flags
:标志位(通常为0
)。
- 输出:
- 成功时返回流索引,失败时返回负值。
4. av_read_frame()
av_read_frame
是 FFmpeg 中一个非常重要的函数,用于从媒体文件(如 MP4、MKV 等)中读取一帧数据(可以是视频帧、音频帧或其他类型的包)。它的作用是从 AVFormatContext
中读取下一个数据包(AVPacket
),并将其存储到指定的 AVPacket
结构中。
1. 函数原型
int av_read_frame(AVFormatContext *fmt_ctx, AVPacket *pkt);
-
参数:
fmt_ctx
:AVFormatContext
指针,表示媒体文件的上下文。pkt
:AVPacket
指针,用于存储读取到的数据包。
-
返回值:
- 成功时返回
0
。 - 如果到达文件末尾,返回
AVERROR_EOF
。 - 如果发生错误,返回负的错误代码。
- 成功时返回
2. 功能说明
av_read_frame
的作用是从媒体文件中读取下一个数据包(AVPacket
),并将其存储到 pkt
中。数据包可以是:
- 视频帧(如 H.264 帧)。
- 音频帧(如 AAC 帧)。
- 其他类型的包(如字幕或元数据)。
每次调用 av_read_frame
时,它会从文件中读取一个完整的数据包,并将其填充到 pkt
中。读取的数据包需要后续通过解码器(AVCodecContext
)进行解码。
3. 使用步骤
以下是使用 av_read_frame
的典型步骤:
1. 打开媒体文件并初始化 AVFormatContext
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
avformat_find_stream_info(fmt_ctx, NULL);
2. 准备 AVPacket
AVPacket pkt;
av_init_packet(&pkt);
3. 循环读取数据包
while (av_read_frame(fmt_ctx, &pkt) >= 0) {// 检查数据包属于哪个流if (pkt.stream_index == video_stream_index) {// 处理视频帧} else if (pkt.stream_index == audio_stream_index) {// 处理音频帧}// 释放数据包av_packet_unref(&pkt);
}
4. 释放资源
avformat_close_input(&fmt_ctx);
4. 关键点
1. AVPacket
的生命周期
av_read_frame
会为pkt
分配内存并填充数据。- 使用完
pkt
后,必须调用av_packet_unref
释放其内存,否则会导致内存泄漏。
2. 流索引(stream_index
)
- 每个数据包都属于某个流(视频流、音频流等),通过
pkt.stream_index
可以确定数据包所属的流。 - 流的索引可以通过
AVFormatContext
中的streams
数组获取。
3. 数据包的时间戳
- 数据包中包含时间戳(PTS 和 DTS),用于同步音视频。
- 时间戳的单位是流的时间基(
AVStream->time_base
),需要通过av_q2d
转换为秒。
5. av_seek_frame()
在 FFmpeg 中,av_seek_frame()
是用于在输入流中定位到指定时间戳或帧索引的核心函数。它允许你在处理音视频流时跳转到特定位置,广泛应用于播放器、编辑器等场景。以下是详细解析:
1. 函数原型
int av_seek_frame(AVFormatContext *fmt_ctx, int stream_idx, int64_t timestamp, int flags);
参数
fmt_ctx
: 输入流的上下文(AVFormatContext*
),表示要操作的媒体文件或流。stream_idx
: 需要操作的流的索引(如视频流为0
,音频流为1
)。若为-1
,表示操作所有流。timestamp
: 目标时间戳(单位由流的time_base
定义,如微秒)。flags
: 控制 seek 行为的标志位,例如:AVSEEK_FLAG_BACKWARD
: 向后搜索(最近的匹配位置)。AVSEEK_FLAG_FORWARD
: 向前搜索(第一个匹配位置)。AVSEEK_FLAG_FRAME精确
: 精确匹配帧边界。AVSEEK_FLAG_ANY
: 允许任何近似值。
返回值
• ≥0: 成功,返回新的时间戳位置。
• <0: 失败,返回错误码(如 AVERROR_EOF
)。
2. 核心功能
- 时间戳定位:将播放头移动到指定的时间戳(如
10秒
)。 - 帧索引定位:直接跳转到指定帧(如第
100
帧)。 - 流同步:确保多个流(视频+音频)同步到同一时间点。
3. 使用示例
场景 1:跳转到指定时间(秒)
AVFormatContext *fmt_ctx = ...; // 初始化的输入流上下文
int video_stream_idx = ...; // 视频流索引// 跳转到第 5 秒(需转换为时间戳)AVRational time_base = fmt_ctx->streams[video_stream_idx]->time_base;int64_t target_ts = 5 * av_q2d(time_base); // 5秒 = 5 / 1 (假设 time_base=1/1)int ret = av_seek_frame(fmt_ctx, video_stream_idx, target_ts, AVSEEK_FLAG_BACKWARD);
if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Seek failed\n");
} else {av_log(NULL, AV_LOG_INFO, "Seeked to %lld microseconds\n", ret);
}
场景 2:跳转到指定帧
// 跳转到第 100 帧(仅视频流支持)
int frame = 100;
ret = av_seek_frame(fmt_ctx, video_stream_idx, frame, AVSEEK_FLAG_FRAME精确);
4. 关键注意事项
1. 时间基转换
- 时间戳单位:
timestamp
的单位由流的time_base
决定(如AV_TIME_BASE
表示微秒)。 - 转换公式:
int64_t timestamp = seconds * av_q2d(time_base);
// 或 av_rescale_q(seconds, AV_TIME_BASE, time_base)
2. 流索引处理
- 单一流操作:明确指定
stream_idx
(如视频或音频流)。 - 多流同步:若需同步多个流,需分别对每个流调用
av_seek_frame()
。
3. 错误处理
- 检查返回值:失败时可能返回
AVERROR_EOF
(未找到位置)或AVERROR_IO
(I/O 错误)。 - 流状态:确保流未被关闭,且处于可seek状态(如
AVFS_SEEKABLE
)。
4. 性能优化
- 批量 seek:避免频繁调用,可结合
av_read_frame()
的AVFRAME_FLAG Sebastian
标志读取多帧。 - 硬件加速:某些解码器(如 NVIDIA NVDEC)可能不支持随机访问,需特殊处理。
5. 高级场景
多流同步
// 同步视频和音频流到同一时间戳
int videoStream = ...;
int audioStream = ...;
int64_t target_ts = ...;av_seek_frame(fmt_ctx, videoStream, target_ts, 0);
av_seek_frame(fmt_ctx, audioStream, target_ts, 0);
动态调整播放速度
// 加速播放(2倍速)
int64_t new_ts = av_rescale_q(current_ts, fmt_ctx->streams[0]->time_base, av_make_q(1, 2)); // 时间缩放因子为 0.5
av_seek_frame(fmt_ctx, 0, new_ts, 0);
6. 常见问题
-
为什么seek后无法读取到数据?
- 缓冲区未刷新:调用
av_flush_packets(fmt_ctx)
清空输入缓冲区。 - 流未seekable:某些流(如直播流)不支持随机访问。
- 缓冲区未刷新:调用
-
如何实现逐帧播放?
int frame = 0; while (frame < total_frames) {av_seek_frame(fmt_ctx, videoStream, frame, AVSEEK_FLAG_FRAME精确);AVPacket pkt;av_init_packet(&pkt);avcodec_decode_video2(...); // 读取当前帧frame++; }
-
seek到帧边界的问题
- 使用
AVSEEK_FLAG_FRAME精确
确保定位到帧起始位置。
- 使用
常用数据结构
AVFormatContext:媒体格式的全局管理者
作用
- 管理容器格式:存储媒体文件的容器信息(如MP4、MKV、FLV等)。
- 封装流信息:包含文件中所有流(
AVStream
)的元数据。 - 控制输入/输出:用于解封装(demuxing)或封装(muxing)操作。
关键字段
typedef struct AVFormatContext {const AVClass *av_class; // 类信息(用于日志和回调)AVInputFormat *iformat; // 输入格式(解封装时使用)AVOutputFormat *oformat; // 输出格式(封装时使用)AVIOContext *pb; // I/O上下文(文件或网络读写)unsigned int nb_streams; // 流的数量AVStream **streams; // 流数组(每个元素对应一个AVStream)char filename[1024]; // 文件名或URLint64_t duration; // 文件总时长(微秒)int64_t bit_rate; // 全局比特率(bps)AVDictionary *metadata; // 元数据(标题、作者等)
} AVFormatContext;
典型用法
// 打开输入文件
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);// 读取流信息
avformat_find_stream_info(fmt_ctx, NULL);// 遍历所有流,找到视频流索引
int video_stream_idx = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {video_stream_idx = i;break;}
}// 关闭并释放资源
avformat_close_input(&fmt_ctx);
AVCodecParameters
AVCodecParameters
是 FFmpeg 中用于描述编解码器参数的结构体。它在 FFmpeg 的较新版本中取代了旧的 AVCodecContext
,用于存储音视频流的编解码信息。AVCodecParameters
是一个更轻量级的结构体,专注于存储编解码器的参数,而不包含编解码器的状态或运行时数据。
AVCodecParameters
用于描述音视频流的编解码属性,例如:
- 编码格式(如 H.264、AAC)。
- 视频的分辨率、帧率。
- 音频的采样率、声道数。
- 其他编解码相关的参数。
它的主要目的是在解复用(Demuxing)时提取流的编解码信息,而不需要初始化完整的编解码器上下文(AVCodecContext
)。
1. AVCodecParameters
的主要字段
以下是 AVCodecParameters
中一些重要的字段:
字段名 | 类型 | 描述 |
---|---|---|
codec_type | AVMediaType | 媒体类型(视频、音频、字幕等)。例如:AVMEDIA_TYPE_VIDEO 表示视频。 |
codec_id | AVCodecID | 编解码器 ID(如 AV_CODEC_ID_H264 表示 H.264 编码)。 |
format | int | 像素格式(视频)或采样格式(音频)。例如:AV_PIX_FMT_YUV420P 。 |
width / height | int | 视频的宽度和高度(以像素为单位)。 |
sample_rate | int | 音频的采样率(如 44100 Hz)。 |
channels | int | 音频的声道数(如 2 表示立体声)。 |
channel_layout | uint64_t | 音频的声道布局(如 AV_CH_LAYOUT_STEREO 表示立体声)。 |
bit_rate | int64_t | 流的比特率(单位:比特/秒)。 |
extradata | uint8_t* | 编解码器特定的额外数据(如 H.264 的 SPS/PPS)。 |
extradata_size | int | 额外数据的大小。 |
2. AVCodecParameters
的使用场景
AVCodecParameters
通常在以下场景中使用:
-
解复用(Demuxing):
- 当从容器(如 MP4、MKV)中读取音视频流时,
AVFormatContext
会为每个流分配一个AVStream
,而AVStream
中的codecpar
字段就是AVCodecParameters
。 - 通过
AVCodecParameters
,可以获取流的编解码信息,而不需要初始化编解码器。
- 当从容器(如 MP4、MKV)中读取音视频流时,
-
编码/解码前的准备:
- 在初始化编解码器(
AVCodecContext
)之前,可以使用AVCodecParameters
中的信息来配置编解码器。
- 在初始化编解码器(
-
流的复制或转封装:
- 在转封装(Remuxing)时,可以直接将
AVCodecParameters
从一个流复制到另一个流,而不需要重新解析编解码信息。
- 在转封装(Remuxing)时,可以直接将
3. AVCodecParameters
与 AVCodecContext
的区别
-
AVCodecParameters
:- 仅存储编解码器的参数(如格式、分辨率、采样率等)。
- 不包含编解码器的状态或运行时数据。
- 更轻量级,适合在解复用或转封装时使用。
-
AVCodecContext
:- 存储编解码器的参数和状态。
- 包含编解码器的运行时数据(如帧缓冲区、编码延迟等)。
- 需要在编解码时使用。
AVStream:媒体流的详细信息
作用
- 描述单个流:每个
AVStream
对应一个媒体流(如视频、音频、字幕)。 - 存储流参数:包含流的编解码参数(如分辨率、采样率)、时间基(
time_base
)等。
关键字段
typedef struct AVStream {int index; // 流索引(唯一标识)AVCodecParameters *codecpar; // 编解码参数(已过时)AVRational time_base; // 时间基(理解成分数就行了)int64_t duration; // 流的总时长(单位:time_base)AVRational avg_frame_rate; // 平均帧率(视频流)
} AVStream;
AVRational time_base
是 FFmpeg 中用于表示时间基的结构体。时间基是一个分数,形式为 num/den
,其中 num
是分子,den
是分母。它定义了时间的基本单位,用于将时间值转换为秒或其他时间单位。
具体解释:
时间基的定义
时间基是一个分数,形式为 AVRational {num, den},表示每个时间戳的单位是 num/den 秒。
例如:
-
如果 time_base = {1, 1000},那么每个时间戳的单位是 1/1000 秒(即 1 毫秒)。
-
如果 time_base = {1, 90000},那么每个时间戳的单位是 1/90000 秒(常见于 MPEG-TS 流)。
那么该帧的实际时间可以通过公式计算:double seconds = timestamp * av_q2d(time_base);
其中
av_q2d
是 FFmpeg 提供的函数,用于将AVRational
转换为浮点数。 -
示例:
假设time_base = {1, 1000}
,即1/1000
,表示时间单位是毫秒。如果某个帧的时间戳是5000
,那么该帧的实际时间是:double seconds = 5000 * (1.0 / 1000) = 5.0 秒
-
在
AVStream
中的意义:time_base
是流的时间基准,用于解释该流中的时间戳。- 例如,视频流的时间基可能是
1/90000
(常见于 MPEG-TS 流),而音频流的时间基可能是1/44100
(CD 音质)。
-
与其他字段的关系:
duration
字段表示流的总时长,单位是time_base
。例如,如果duration = 90000
且time_base = {1, 1000}
,那么流的总时长是 90 秒。avg_frame_rate
是视频流的平均帧率,也是一个AVRational
,表示每秒的帧数。
AVPacket:编码后的数据包
作用
- 存储压缩数据:保存从媒体文件读取的编码后的数据(如一个视频帧或音频帧)。
- 携带时间信息:包含解码时间戳(DTS)和显示时间戳(PTS)。
关键字段
typedef struct AVPacket {AVBufferRef *buf;uint8_t *data; // 数据指针(压缩数据)int size; // 数据大小int64_t pts; // 表示数据应被显示的时间点 (num/den)int64_t dts; // 表示数据应被解码的时间点(num/den)int stream_index; // 所属流的索引int flags; // 标志位(关键帧等)
} AVPacket;
av_packet
的生命周期管理
函数 | 作用 | 内存操作 |
---|---|---|
av_packet_alloc() | 分配新包 | 分配内存,引用计数初始化为 0 |
av_packet_clone() | 克隆包(共享缓冲区) | 引用计数不变 |
av_buffer_ref() | 增加缓冲区引用 | 引用计数 +1 |
av_packet_unref() | 释放包 | 引用计数 -1,释放内存 |
av_packet_free() | 强制释放包 | 直接释放内存(不依赖引用计数) |
三者的协作流程
-
初始化容器:
- 通过
AVFormatContext
打开输入文件,获取全局信息。 - 遍历
AVFormatContext->streams
获取各个AVStream
。
- 通过
-
处理数据包:
- 使用
av_read_frame
读取AVPacket
。 - 根据
AVPacket->stream_index
找到对应的AVStream
。 - 将
AVPacket
送入解码器(需结合AVCodecContext
)。
- 使用
-
时间戳转换:
- 将
AVPacket
的pts
和dts
转换为实际时间:double timestamp_sec = pkt.pts * av_q2d(stream->time_base);
- 将
-
资源释放:
- 使用
avformat_close_input
释放AVFormatContext
。 - 使用
av_packet_unref
释放AVPacket
。
- 使用
总结
- AVFormatContext:媒体文件的全局管理器,负责解封装和流信息存储。
- AVStream:单个流的详细信息,包含编解码参数和时间基。
- AVPacket:编码后的数据包,携带压缩数据和时间戳。
三者协作实现媒体文件的读取、处理和写入,是FFmpeg处理流程的核心结构体。
AVPacket的关键函数
1. av_packet_alloc()
作用
动态分配一个空的 AVPacket
,初始化 buf
数组和元数据。
函数原型
AVPacket *av_packet_alloc(int buf_count);
参数
• buf_count
: 预分配的 buf
数组长度(需 ≥ 数据平面数)。
返回值
• 成功返回指向新分配的 AVPacket
,失败返回 NULL
。
示例
// 分配一个支持 3 数据平面的包(如视频 YUV420P)
AVPacket *pkt = av_packet_alloc(3);
if (!pkt) {av_log(NULL, AV_LOG_ERROR, "Allocation failed\n");exit(1);
}// 使用后释放
av_packet_unref(pkt); // 自动释放内存
2. av_packet_clone()
作用
深度克隆现有 AVPacket
,包括 buf
引用、时间戳、流索引等所有字段。
函数原型
int av_packet_clone(AVPacket *src, AVPacket *dst, int buf_count);
参数
• src
: 源数据包。
• dst
: 目标数据包(需已通过 av_packet_alloc()
分配)。
• buf_count
: 目标 buf
数组容量(需 ≥ src->buf_count
)。
返回值
• 成功返回 0
,失败返回错误码(如 AVERROR(ENOMEM)
)。
示例
AVPacket *src_pkt, *dst_pkt;
av_packet_alloc(&dst_pkt, src_pkt->buf_count); // 预分配缓冲区int ret = av_packet_clone(src_pkt, dst_pkt, src_pkt->buf_count);
if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "Clone failed\n");
}// 增加引用计数(若需长期保留)
for (int i = 0; i < dst_pkt->buf_count; i++) {av_buffer_ref(dst_pkt->buf[i]);
}av_packet_unref(dst_pkt); // 自动释放
3. av_packet_ref()
作用
增加 AVBufferRef
的引用计数,确保缓冲区不会被意外释放。
函数原型
void av_buffer_ref(AVBufferRef *buf);
参数
• buf
: 需要增加引用的 AVBufferRef
。
使用场景
• 克隆后保留数据:克隆 AVPacket
后,若需长期使用其缓冲区,需手动调用 av_buffer_ref()
。
• 多线程共享:在多线程环境中,确保每个线程对缓冲区的引用合法。
1. 为什么克隆后需要手动调用 av_buffer_ref()
?
引用计数的作用
- 共享缓冲区:
AVPacket
克隆后,缓冲区引用是共享的(即克隆后的buf
数组直接指向源包的AVBufferRef
)。 - 引用计数规则:
- 引用计数 (
refcount
):表示当前有多少个AVBufferRef
指向同一块内存。 - 释放条件:当
refcount
降为0
时,FFmpeg 会自动释放缓冲区内存。
- 引用计数 (
克隆后的风险
- 示例场景:
AVPacket *src_pkt = ...; // 原始数据包,buf->refcount=1 AVPacket *clone_pkt = av_packet_clone(src_pkt, ...); // 克隆后,clone_pkt->buf 的 refcount=1
- 问题:
如果此时源包src_pkt
被释放(av_packet_unref(src_pkt)
),其buf
的refcount
会减到0
,导致缓冲区被销毁。此时clone_pkt
仍然指向已释放的内存,引发未定义行为(如崩溃或数据错误)。
解决方案
- 手动增加引用:
通过av_buffer_ref(clone_pkt->buf[i])
显式增加引用计数,确保缓冲区不会被意外释放:for (int i = 0; i < clone_pkt->buf_count; i++) {av_buffer_ref(clone_pkt->buf[i]); // refcount +=1 }
- 引用计数变化:
• 克隆后:buf->refcount=1
(共享)。
• 增加引用后:buf->refcount=2
,即使源包被释放,缓冲区仍保留。
2. 为什么多线程环境需要确保引用合法?
线程安全问题
- 竞态条件:
多个线程可能同时操作同一缓冲区的引用计数(如一个线程释放内存,另一个线程正在读取数据)。 - 原子操作:
FFmpeg 的refcount
使用原子操作(如AV_ATOMIC_INC
和AV_ATOMIC_DEC
)确保增减操作的原子性,但用户代码仍需遵守规则。
关键规则
- 每个线程必须独立管理引用:
- 如果线程 A 持有缓冲区的引用,线程 B 不能直接释放它。
- 克隆后需显式增加引用:
- 即使缓冲区由 FFmpeg 内部管理(
owner=1
),多线程环境下仍需调用av_buffer_ref()
,避免其他线程误释放。
- 即使缓冲区由 FFmpeg 内部管理(
- 使用
av_packet_unref()
而非直接free
:- 始终通过
av_packet_unref()
释放AVPacket
,由其自动处理引用计数递减。
- 始终通过
示例场景
// 线程 1:克隆数据包并处理
AVPacket *clone_pkt = av_packet_clone(src_pkt, ...);
av_buffer_ref(clone_pkt->buf[0]); // 增加引用// 线程 2:释放源包(可能导致问题!)
av_packet_unref(src_pkt); // 如果 clone_pkt 未增加引用,此处会释放缓冲区
解决方案
- 线程内独立引用:
每个线程在克隆后必须自行增加引用,并在结束时释放:// 线程 1 AVPacket *clone_pkt = av_packet_clone(src_pkt, ...); av_buffer_ref(clone_pkt->buf[0]); // 线程 1 的引用 process(clone_pkt); av_buffer_unref(clone_pkt->buf[0]); // 线程 1 释放引用 av_packet_unref(clone_pkt);// 线程 2 av_packet_unref(src_pkt); // 安全释放(假设 src_pkt 无其他引用)
3. 深层原理:FFmpeg 的内存管理策略
AVBufferRef 的设计
- 引用计数 (
refcount
):- 初始值为
1
(由分配者持有)。 - 每次
av_buffer_ref()
调用,refcount
增加;每次av_buffer_unref()
调用,refcount
减少。
- 初始值为
- 所有者标志 (
owner
):owner=1
:缓冲区由 FFmpeg 管理,av_buffer_unref()
会释放内存。owner=0
:用户管理内存,av_buffer_unref()
仅减少引用计数,不释放内存。
克隆操作的副作用
- 浅拷贝:
av_packet_clone()
是浅拷贝,buf
数组直接引用源包的AVBufferRef
。 - 引用计数共享:克隆后的
buf
引用计数与源包一致,不自动增加。
4. 最佳实践总结
场景 | 正确操作 | 错误操作 | 结果 |
---|---|---|---|
克隆后长期使用 | av_buffer_ref(clone_pkt->buf[i]) | 直接使用,不增加引用 | 缓冲区被源包释放,导致崩溃或数据错误 |
多线程共享数据包 | 每个线程独立调用 av_buffer_ref() 和 av_buffer_unref() | 所有线程共享同一个引用 | 竞态条件,内存泄漏或崩溃 |
释放数据包 | av_packet_unref(pkt) | 直接 free(pkt) 或 av_free(pkt) | 引用计数未正确递减,内存泄漏 |
4. av_packet_free()
作用
释放 AVPacket
及其关联的 AVBufferRef
,自动递减引用计数。
函数原型
void av_packet_free(AVPacket *pkt);
注意事项
• 引用计数规则:
• 若 buf
由 FFmpeg 内部管理(buf->owner=1
),调用 av_packet_free()
会自动释放。
• 若 buf
由用户管理(如硬件解码器返回的 GPU 缓冲区),需手动释放。
• 替代函数:推荐使用 av_packet_unref()
,它会自动处理引用计数。
示例
AVPacket *pkt = av_packet_alloc(3);
// ... 使用 pkt ...
av_packet_free(pkt); // 释放内存
5. av_init_packet()
作用
初始化 AVPacket
结构体,设置默认值(如 size=0
、pts=dts=0
)。
函数原型
void av_init_packet(AVPacket *pkt);
与 av_packet_alloc()
的区别
• 无需分配内存:仅初始化现有结构体的字段。
• 典型用法:在复用已分配的 AVPacket
时调用(如循环处理数据包)。
示例
AVPacket pkt;
av_init_packet(&pkt); // 初始化
pkt.buf_count = 3; // 设置 buf 数组长度
// ... 填充数据 ...
av_packet_unref(&pkt); // 释放