8.1.3 收发分离:解码 API 的状态机(The Send/Receive State Machine)

上一节把 avcodec 定位成 "压缩与原始之间的变换"。这个变换的 API 该怎么长?直觉的回答是 decode(packet) → frame——喂一个包,还一帧。FFmpeg 早年的 API 确实长这样,但它早已作古;现代答案是一对分工明确的函数:avcodec_send_packet()avcodec_receive_frame(),输入与输出彻底解耦 [1]。这次拆分的背后,是解码器状态机本质对 API 设计的反噬。

一进一出是错觉

天真的 decode(packet) → frame 模型,预设了输入与产出一一对应。可 8.1.1 已经拆过这台机器的记忆:B 帧要重排,参考帧要缓冲,参数集要生效——解码器内部有一条看不见的队列。每个输入包通常产一帧,但完全可能产 0 帧或多于 1 帧 [1]

  • 0 帧:包进了重排缓冲,帧序未到,暂不放行
  • 多帧:某些解码器内部攒了批处理,一次吐出多帧;排干阶段更是成批放出

把进出两端焊死在一个函数里,这些情形就得靠蹩脚的输出参数与特殊返回值硬挤——API 拧巴,调用方更拧巴。解耦之后,一切顺理成章:send 是往队列里推,receive 是从队列里拉,两端各问各的

EAGAIN:背压的语言

解耦之后的沟通暗号是一个特殊的错误码:AVERROR(EAGAIN)。它的语义随方向而变 [1]

  • receive 返回 EAGAIN:"内部队列见底了,喂新包来"——该回去 send 了
  • send 返回 EAGAIN:"内部队列满了,先取货"——该先去 receive 了

标准的解码循环因此写成一支推拉双人舞 [1]

// ① 喂包:send_packet(pkt)
// ② 取帧:循环 receive_frame 直到 EAGAIN
// ③ 回到 ①
ret = avcodec_send_packet(ctx, pkt);
while (ret >= 0) {
    ret = avcodec_receive_frame(ctx, frame);
    if (ret == AVERROR(EAGAIN)) break;   // 队列见底,回去喂包
    if (ret < 0) /* 出错处理 */;
    /* 消费 frame */
}

背压(backpressure)这个概念的妙处在于:流控的主动权交还给了解码器。它知道自己队列的深浅,调用方只需听话地推拉,无需猜测内部状态。

两条容易被踩的工程语义

其一,起播延迟。 解码器刚开张时,可能连吃多个包却一帧不吐——内部缓冲未填满,重排序列未凑齐。官方文档对此有明确预警:开解码初期连续喂包而无产出,是正常现象而非卡死 [1]。直播起播时那零点几秒的黑屏,一部分账就记在这里。

其二,排干(draining)。 输入走到尽头时,队列里还压着没放行的帧——尤其是 B 帧重排缓冲里的存货。收尾的正确姿势是 send 一个 NULL 包 宣告 "不再喂了",解码器进入排干模式,随后 receive 会把残留帧一粒不剩地吐完,直到返回 EOF [1]。少了这一步,片尾的最后几帧就会无声无息地丢在队列里——多少 "视频转完缺最后一秒" 的悬案,元凶都在此处。

最后是一颗定心丸:FFmpeg 保证 同一轮里 send 与 receive 不会同时返回 EAGAIN [1]——队列不可能既满又空,推拉循环永远不会陷入"两头都让我等"的死锁。状态机的设计自洽,到此闭环。

小结

收发分离的 API 是解码器状态机本质的水到渠成:队列客观存在,不如把它摆上台面;产出不必一比一,不如让两端各自提问;EAGAIN 背压、起播延迟、NULL 排干,每条语义都对应着一类真实的工程事故。接口层看懂了,还剩最后一块硬骨头——时间。PTS 与 DTS 的纠葛、seek 时的状态重建,是解码管线里最深的水域。下一节,我们去蹚。

Copyright © Since 2021 李述博 (Arikan.Li) , All Rights Reserved all right reserved,powered by GitbookLast Updated: 2026-08-26 08:25:15

results matching ""

    No results matching ""