第一阶段:初始化和打开流
这一步通常在播放器或处理程序的初始化阶段完成。
- 打开输入文件(Open Input):
- 使用
avformat_open_input(&fmt_ctx, filename, NULL, NULL)打开一个媒体文件(如 MP4、MKV)或网络流。这个函数会返回一个AVFormatContext结构体,它是整个媒体文件的容器,包含了文件的所有信息(流信息、元数据等)。
- 使用
- 查找流信息(Find Stream Info):
- 使用
avformat_find_stream_info(fmt_ctx, NULL)来读取文件的一部分数据包并分析出文件中包含哪些流(Stream),比如视频流、音频流、字幕流。它会填充AVFormatContext中的streams数组。
- 使用
- 查找并打开视频解码器(Find and Open Codec):
- 遍历
fmt_ctx->streams,找到类型(codecpar->codec_type)为AVMEDIA_TYPE_VIDEO的流(AVStream)。通常我们选择第一个视频流。 - 从找到的
AVStream的codecpar(编解码器参数)中获取流的编码格式(如 H.264、HEVC、VP9)。 - 使用
avcodec_find_decoder(stream->codecpar->codec_id)根据编码格式查找对应的解码器(AVCodec)。 - 创建一个解码器上下文(
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);)。 - 使用
avcodec_parameters_to_context(codec_ctx, stream->codecpar)将流的参数复制到解码器上下文中。 - 最后使用
avcodec_open2(codec_ctx, codec, NULL)打开解码器。现在解码器已经初始化完毕,准备好接收AVPacket了。
- 遍历
第二阶段:循环读取与解码
这是主循环过程,不断从文件中读取数据包,送入解码器,并取出帧。
- 读取数据包(Read Packet):
- 在一个循环中,调用
av_read_frame(fmt_ctx, &pkt)。这个函数会从AVFormatContext中读取下一个数据包(可能是视频、音频或其他)。 - 它返回的是一个
AVPacket,这是一个压缩的、编码后的数据单元。注意:对于视频来说,一个AVPacket可能包含一帧完整的数据(如一个完整的 H.264 I帧),也可能包含一部分帧数据(如一个 H.264 P帧片段),这取决于编码格式和封装格式。
- 在一个循环中,调用
- 判断流类型(Check Stream Index):
- 检查读取到的
pkt.stream_index是否等于我们之前找到的视频流索引(video_stream_index)。如果不等,说明这个包是音频或其他数据,可以跳过或送给相应的音频解码器处理。
- 检查读取到的
- 发送数据包到解码器(Send Packet):
- 对于视频包,调用
avcodec_send_packet(codec_ctx, &pkt)。 - 这个函数将
AVPacket(压缩数据)送入解码器的内部缓冲区。解码器并不会立即解码它,而是将其放入一个队列中等待处理。 - 重要:送入一个
AVPacket后,不一定就能立刻取出一个AVFrame。解码器可能会缓存多个包,尤其是在处理有 B 帧的流时。 - 发送完成后,应立即调用
av_packet_unref(&pkt)来释放数据包占用的资源,因为数据已被解码器缓存。
- 对于视频包,调用
- 从解码器接收帧(Receive Frame):
AVERROR(EAGAIN): 表示解码器需要更多的输入数据(即更多的AVPacket)才能输出一帧。此时你应该回到第 1 步,继续发送新的AVPacket。AVERROR_EOF: 表示解码器已刷新(flush),所有帧都已输出。0: 成功! 此时frame变量中存储了一帧完整的、解码后的图像数据。你可以处理这个帧了(如显示、保存为图片、进行滤镜处理等)。- 其他负值:表示发生了错误。
- 在一个循环中调用
avcodec_receive_frame(codec_ctx, frame)。 - 这个函数尝试从解码器的内部缓冲区中获取一个已经解码完成的
AVFrame。AVFrame 是解码后的、原始的(如 YUV420P、RGB)视频帧数据。 - 这个函数的返回值需要仔细处理:
- 处理解码出的帧(Process Frame):
data[]: 一个指针数组,指向实际的图像平面数据(如 Y、U、V 三个平面)。linesize[]: 一个数组,表示每个平面一行数据的大小(字节数)。注意由于内存对齐,这个值可能大于图像的宽度。width,height: 图像的宽高。format: 像素格式(如AV_PIX_FMT_YUV420P)。pts: 显示时间戳(Presentation Timestamp),告诉你这一帧应该在什么时间显示。
- 现在你拥有了一个
AVFrame,它的成员变量包含了丰富的帧信息: - 你可以访问这些数据来进行后续操作。
第三阶段:刷新解码器(Flushing)
在文件读取完毕(av_read_frame 返回 AVERROR_EOF)后,解码器的内部缓冲区里可能还有缓存的几帧数据(由于延迟解码)。你需要“刷新”解码器来取出这些剩余的帧。
- 发送一个
NULL包:- 调用
avcodec_send_packet(codec_ctx, NULL)。这个NULL包是一个信号,告诉解码器“没有更多的输入了”,请开始处理并输出所有缓存的帧。
- 调用
- 继续接收帧:
- 像正常循环一样,继续反复调用
avcodec_receive_frame(codec_ctx, frame)。 - 你会陆续接收到最后几帧,直到函数返回
AVERROR_EOF,这意味着解码器真的空了,流程结束。
- 像正常循环一样,继续反复调用
总结与关键点
AVPacketvsAVFrame: Packet 是压缩的数据,Frame 是原始的数据。这是最重要的概念。- 发送/接收 API:
avcodec_send_packet()和avcodec_receive_frame()是现代的编解码 API(自 FFmpeg 3.1 起)。它替代了旧版avcodec_decode_video2()函数,采用了更清晰的“发送-接收”模式。 - 异步性: 发送一个 Packet 不一定能接收到一个 Frame。可能需要发送多个 Packet 才能收到一个 Frame,或者发送一个 Packet 后能收到多个 Frame(很少见)。接收函数返回
EAGAIN是正常流程的一部分。 - 内存管理: 务必使用
av_packet_unref()和av_frame_unref()来正确管理 Packet 和 Frame 的内存,否则会造成严重的内存泄漏。 - 多线程: 解码器上下文通常不是线程安全的。如果你要解码多个流,最好为每个流创建独立的解码器上下文。
这个流程是使用 FFmpeg 进行视频处理的基础,无论是简单的播放器、转码工具还是复杂的视频分析应用,都建立在这个核心流程之上。
微信扫描下方的二维码阅读本文

Comments NOTHING