avformat_open_input()
AVFormatContext
:分配并初始化 AVFormatContext
结构体,用于存储媒体文件的元数据和流信息。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
包含媒体文件的元数据和流信息。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
数组包含所有流的信息(如视频、音频、字幕等)。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
)。av_read_frame()
av_read_frame
是 FFmpeg 中一个非常重要的函数,用于从媒体文件(如 MP4、MKV 等)中读取一帧数据(可以是视频帧、音频帧或其他类型的包)。它的作用是从 AVFormatContext
中读取下一个数据包(AVPacket
),并将其存储到指定的 AVPacket
结构中。
int av_read_frame(AVFormatContext *fmt_ctx, AVPacket *pkt);
参数:
fmt_ctx
:AVFormatContext
指针,表示媒体文件的上下文。pkt
:AVPacket
指针,用于存储读取到的数据包。返回值:
0
。AVERROR_EOF
。av_read_frame
的作用是从媒体文件中读取下一个数据包(AVPacket
),并将其存储到 pkt
中。数据包可以是:
每次调用 av_read_frame
时,它会从文件中读取一个完整的数据包,并将其填充到 pkt
中。读取的数据包需要后续通过解码器(AVCodecContext
)进行解码。
以下是使用 av_read_frame
的典型步骤:
AVFormatContext
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
avformat_find_stream_info(fmt_ctx, NULL);
AVPacket
AVPacket pkt;
av_init_packet(&pkt);
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);
}
avformat_close_input(&fmt_ctx);
AVPacket
的生命周期av_read_frame
会为 pkt
分配内存并填充数据。pkt
后,必须调用 av_packet_unref
释放其内存,否则会导致内存泄漏。stream_index
)pkt.stream_index
可以确定数据包所属的流。AVFormatContext
中的 streams
数组获取。AVStream->time_base
),需要通过 av_q2d
转换为秒。av_seek_frame()
在 FFmpeg 中,av_seek_frame()
是用于在输入流中定位到指定时间戳或帧索引的核心函数。它允许你在处理音视频流时跳转到特定位置,广泛应用于播放器、编辑器等场景。以下是详细解析:
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
)。
10秒
)。100
帧)。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);
}
// 跳转到第 100 帧(仅视频流支持)
int frame = 100;
ret = av_seek_frame(fmt_ctx, video_stream_idx, frame, AVSEEK_FLAG_FRAME精确);
timestamp
的单位由流的 time_base
决定(如 AV_TIME_BASE
表示微秒)。int64_t timestamp = seconds * av_q2d(time_base);
// 或 av_rescale_q(seconds, AV_TIME_BASE, time_base)
stream_idx
(如视频或音频流)。av_seek_frame()
。AVERROR_EOF
(未找到位置)或 AVERROR_IO
(I/O 错误)。AVFS_SEEKABLE
)。av_read_frame()
的 AVFRAME_FLAG Sebastian
标志读取多帧。// 同步视频和音频流到同一时间戳
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);
为什么seek后无法读取到数据?
av_flush_packets(fmt_ctx)
清空输入缓冲区。如何实现逐帧播放?
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精确
确保定位到帧起始位置。AVStream
)的元数据。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]; // 文件名或URL
int64_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
是 FFmpeg 中用于存储编解码器参数的核心结构体,存储了流的编解码器参数。,包括视频、音频的分辨率、帧率、编码格式、比特率等关键参数。 它的主要目的是在解复用(Demuxing)时提取流的编解码信息,而不需要初始化完整的编解码器上下文(AVCodecContext
)。
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 |
额外数据的大小。 |
AVCodecParameters
的使用场景AVCodecParameters
通常在以下场景中使用:
解复用(Demuxing):
AVFormatContext
会为每个流分配一个 AVStream
,而 AVStream
中的 codecpar
字段就是 AVCodecParameters
。AVCodecParameters
,可以获取流的编解码信息,而不需要初始化编解码器。编码/解码前的准备:
AVCodecContext
)之前,可以使用 AVCodecParameters
中的信息来配置编解码器。流的复制或转封装:
AVCodecParameters
从一个流复制到另一个流,而不需要重新解析编解码信息。AVCodecParameters
与 AVCodecContext
的区别AVCodecParameters
:
AVCodecContext
:
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
,表示每秒的帧数。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
。三者协作实现媒体文件的读取、处理和写入,是FFmpeg处理流程的核心结构体。
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); // 自动释放内存
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); // 自动释放
增加 AVBufferRef
的引用计数,确保缓冲区不会被意外释放。
void av_buffer_ref(AVBufferRef *buf);
• buf
: 需要增加引用的 AVBufferRef
。
• 克隆后保留数据:克隆 AVPacket
后,若需长期使用其缓冲区,需手动调用 av_buffer_ref()
。
• 多线程共享:在多线程环境中,确保每个线程对缓冲区的引用合法。
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
,即使源包被释放,缓冲区仍保留。线程安全问题
refcount
使用原子操作(如 AV_ATOMIC_INC
和 AV_ATOMIC_DEC
)确保增减操作的原子性,但用户代码仍需遵守规则。关键规则
owner=1
),多线程环境下仍需调用 av_buffer_ref()
,避免其他线程误释放。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 无其他引用)
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
引用计数与源包一致,不自动增加。场景 | 正确操作 | 错误操作 | 结果 |
---|---|---|---|
克隆后长期使用 | av_buffer_ref(clone_pkt->buf[i]) |
直接使用,不增加引用 | 缓冲区被源包释放,导致崩溃或数据错误 |
多线程共享数据包 | 每个线程独立调用 av_buffer_ref() 和 av_buffer_unref() |
所有线程共享同一个引用 | 竞态条件,内存泄漏或崩溃 |
释放数据包 | av_packet_unref(pkt) |
直接 free(pkt) 或 av_free(pkt) |
引用计数未正确递减,内存泄漏 |
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); // 释放内存
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); // 释放