ffmpeg从收到一个视频包到解码出来整个流程

whdahanh 发布于 2025-09-29 165 次阅读


第一阶段:初始化和打开流

这一步通常在播放器或处理程序的初始化阶段完成。

  1. 打开输入文件(Open Input):
    • 使用 avformat_open_input(&fmt_ctx, filename, NULL, NULL) 打开一个媒体文件(如 MP4、MKV)或网络流。这个函数会返回一个 AVFormatContext 结构体,它是整个媒体文件的容器,包含了文件的所有信息(流信息、元数据等)。
  2. 查找流信息(Find Stream Info):
    • 使用 avformat_find_stream_info(fmt_ctx, NULL) 来读取文件的一部分数据包并分析出文件中包含哪些流(Stream),比如视频流、音频流、字幕流。它会填充 AVFormatContext 中的 streams 数组。
  3. 查找并打开视频解码器(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 了。

第二阶段:循环读取与解码

这是主循环过程,不断从文件中读取数据包,送入解码器,并取出帧。

  1. 读取数据包(Read Packet):
    • 在一个循环中,调用 av_read_frame(fmt_ctx, &pkt)。这个函数会从 AVFormatContext 中读取下一个数据包(可能是视频、音频或其他)。
    • 它返回的是一个 AVPacket,这是一个压缩的、编码后的数据单元。注意:对于视频来说,一个 AVPacket 可能包含一帧完整的数据(如一个完整的 H.264 I帧),也可能包含一部分帧数据(如一个 H.264 P帧片段),这取决于编码格式和封装格式。
  2. 判断流类型(Check Stream Index):
    • 检查读取到的 pkt.stream_index 是否等于我们之前找到的视频流索引(video_stream_index)。如果不等,说明这个包是音频或其他数据,可以跳过或送给相应的音频解码器处理。
  3. 发送数据包到解码器(Send Packet):
    • 对于视频包,调用 avcodec_send_packet(codec_ctx, &pkt)
    • 这个函数将 AVPacket(压缩数据)送入解码器的内部缓冲区。解码器并不会立即解码它,而是将其放入一个队列中等待处理。
    • 重要:送入一个 AVPacket 后,不一定就能立刻取出一个 AVFrame。解码器可能会缓存多个包,尤其是在处理有 B 帧的流时。
    • 发送完成后,应立即调用 av_packet_unref(&pkt) 来释放数据包占用的资源,因为数据已被解码器缓存。
  4. 从解码器接收帧(Receive Frame):
    • AVERROR(EAGAIN): 表示解码器需要更多的输入数据(即更多的 AVPacket)才能输出一帧。此时你应该回到第 1 步,继续发送新的 AVPacket
    • AVERROR_EOF: 表示解码器已刷新(flush),所有帧都已输出。
    • 0成功! 此时 frame 变量中存储了一帧完整的、解码后的图像数据。你可以处理这个帧了(如显示、保存为图片、进行滤镜处理等)。
    • 其他负值:表示发生了错误。
    • 在一个循环中调用 avcodec_receive_frame(codec_ctx, frame)
    • 这个函数尝试从解码器的内部缓冲区中获取一个已经解码完成的 AVFrameAVFrame 是解码后的、原始的(如 YUV420P、RGB)视频帧数据
    • 这个函数的返回值需要仔细处理:
  5. 处理解码出的帧(Process Frame):
    • data[]: 一个指针数组,指向实际的图像平面数据(如 Y、U、V 三个平面)。
    • linesize[]: 一个数组,表示每个平面一行数据的大小(字节数)。注意由于内存对齐,这个值可能大于图像的宽度。
    • widthheight: 图像的宽高。
    • format: 像素格式(如 AV_PIX_FMT_YUV420P)。
    • pts: 显示时间戳(Presentation Timestamp),告诉你这一帧应该在什么时间显示。
    • 现在你拥有了一个 AVFrame,它的成员变量包含了丰富的帧信息:
    • 你可以访问这些数据来进行后续操作。

第三阶段:刷新解码器(Flushing)

在文件读取完毕(av_read_frame 返回 AVERROR_EOF)后,解码器的内部缓冲区里可能还有缓存的几帧数据(由于延迟解码)。你需要“刷新”解码器来取出这些剩余的帧。

  1. 发送一个 NULL 包:
    • 调用 avcodec_send_packet(codec_ctx, NULL)。这个 NULL 包是一个信号,告诉解码器“没有更多的输入了”,请开始处理并输出所有缓存的帧。
  2. 继续接收帧:
    • 像正常循环一样,继续反复调用 avcodec_receive_frame(codec_ctx, frame)
    • 你会陆续接收到最后几帧,直到函数返回 AVERROR_EOF,这意味着解码器真的空了,流程结束。

总结与关键点

  • AVPacket vs AVFramePacket 是压缩的数据,Frame 是原始的数据。这是最重要的概念。
  • 发送/接收 APIavcodec_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 进行视频处理的基础,无论是简单的播放器、转码工具还是复杂的视频分析应用,都建立在这个核心流程之上。



微信扫描下方的二维码阅读本文

此作者没有提供个人介绍
最后更新于 2025-09-29