从码流到画面的完整流程

以 BTV 的 H.264 / AVC + HTTP-FLV 线路为例:

  1. 打开输入、探测流信息:读取 FLV 中的配置记录,取得 SPS/PPS、编码类型、尺寸和时间基。
  2. 初始化解码器:把流参数交给解码上下文,选择软件解码或 D3D11 硬件解码。
  3. 持续读取视频包:视频包中的 NALU 按 AVCC 长度前缀排列,由 FFmpeg 完成解析。
  4. 建立解码参考状态:从直播中途接入时,需要取得有效参数和合适的随机接入点,H.264 常用 IDR。
  5. 解码、同步、显示:取得解码画面,根据时间戳安排呈现,再交给 Qt 渲染。

服务端缓存 Sequence Header + 最近一个可解码 GOP,可以让新观众更快取得参数和起始关键帧,从而缩短起播等待。

每一步对应的 FFmpeg 调用

原理步骤 FFmpeg 对应接口或数据
打开直播地址 avformat_open_input()
探测编码、尺寸等信息 avformat_find_stream_info()
选择视频流 av_find_best_stream()
获取 SPS/PPS 配置记录 stream->codecpar->extradata
将流参数交给解码器 avcodec_parameters_to_context()
打开解码器 avcodec_open2()
读取压缩包 av_read_frame()
提交压缩包 avcodec_send_packet()
取得解码画面 avcodec_receive_frame()
定位后重置解码状态 avcodec_flush_buffers()

初始化的核心调用顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 打开输入并探测流信息,取得解码所需的编码参数。
avformat_open_input(&fmt, url, nullptr, &opts);
avformat_find_stream_info(fmt, nullptr);

// 找到视频流,获取包含 SPS/PPS 配置记录的流参数。
int vi = av_find_best_stream(fmt, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
AVCodecParameters* par = fmt->streams[vi]->codecpar;

// 创建解码上下文,并复制编码参数及 extradata。
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
AVCodecContext* ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(ctx, par);

// 完成软硬解相关配置后,打开解码器。
avcodec_open2(ctx, codec, nullptr);

对于 H.264 FLV,可以把输入分成两部分理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Sequence Header
└── AVCDecoderConfigurationRecord
├── SPS
├── PPS
└── NALU 长度字段宽度
↓
codecpar->extradata
↓
解码上下文

后续视频包
└── 长度前缀 + NALU + 长度前缀 + NALU……
↓
AVPacket
↓
avcodec_send_packet()

配置记录提供“怎么解码”,视频包提供“要解码的数据”。

AVCC 和 Annex-B 的边界解析交给 FFmpeg。应用通过统一的流参数和送包接口使用它们;软件解码和硬件解码同样使用这套接口。

BTV 中的线程与起播处理

BTV 将读取、解码和显示分配给不同线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
解封装线程
av_read_frame()
├── 视频包 → 视频队列
├── 音频包 → 音频队列
└── 压缩包 → 缓存/录制旁路

视频线程
打开视频解码器
↓
从视频队列取包
↓
avcodec_send_packet()
↓
循环 avcodec_receive_frame()
↓
按时间戳同步
↓
发布视频帧

Qt 渲染线程
取得视频帧
↓
准备纹理和材质
↓
绘制画面

项目通过 VideoDecoder 封装解码接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 视频线程提交压缩包,返回值交给会话层处理。
int VideoDecoder::send(const AVPacket* packet) {
return avcodec_send_packet(m_context, packet);
}

// 视频线程循环获取解码后的画面。
int VideoDecoder::receive(AVFrame* frame) {
return avcodec_receive_frame(m_context, frame);
}

// 定位后,由视频线程重置解码器内部状态。
void VideoDecoder::flush() {
avcodec_flush_buffers(m_context);
}

送包后循环取帧;取帧返回 EAGAIN 时,继续提供输入。初始化和帧重排序期间,解码器会积累所需数据,再输出画面。

正常起播持续提交视频包,由 FFmpeg 建立解码参考状态。项目已有 H.264 对照验证:保留配置数据、删除首个关键包后,画面在后续关键帧到达时恢复 GPU 解码。

视频回退软件解码,以及特定 AV1 数据错误恢复时,会开启关键包等待:

1
2
3
4
5
6
7
8
// 恢复期间等待封装层标记的关键包,跳过中间非关键包。
if (waitForKeyframe && !(pkt->flags & AV_PKT_FLAG_KEY)) {
av_packet_free(&pkt);
continue;
}

// 关键包到达后恢复正常送包。
waitForKeyframe = false;

AV_PKT_FLAG_KEY 适合应用层进行关键包筛选;H.264 IDR 的精确识别依据是 NALU type 5。

定位、参数变化与起播速度

定位与重新加载

本地 seek 成功后,项目依次处理:

  1. 清空音视频包队列。
  2. 通知音视频解码线程执行 flush。
  3. 重置同步时钟。
  4. 从定位后的数据继续解码,丢弃目标时间之前的画面。

时移回放切换也通过队列清理和解码状态刷新衔接时间线。重新加载输入时,则关闭旧会话并重新建立解码上下文。

推流参数变化

推流端更新 H.264 配置时,FFmpeg 可以通过包的 AV_PKT_DATA_NEW_EXTRADATA 传递新的配置记录,供解码器更新参数。

显示模块根据实际输出帧适配:

  • 从 AVFrame::width/height 获取当前尺寸。
  • 软件路径使用 sws_getCachedContext() 更新转换上下文。
  • 硬件路径根据 GPU 纹理尺寸和格式更新显示资源。

起播参数

BTV 使用以下设置控制流信息探测:

1
2
3
4
5
6
7
8
// 设置流信息分析时长参数,单位为微秒。
av_dict_set(&opts, "analyzeduration", "500000", 0);

// 设置探测数据量,单位为字节。
av_dict_set(&opts, "probesize", "500000", 0);

// 在可推导的情况下生成缺失的 PTS,供播放同步使用。
av_dict_set(&opts, "fflags", "genpts", 0);

起播耗时由 网络连接、流信息探测、关键帧到达和解码初始化共同决定。

播放过程中,项目还通过队列容量和时间戳同步控制积压:画面提前时等待,明显落后时丢弃过时画面,追赶播放时钟。

软解与硬解

软硬解共用:

1
流参数 → 打开解码器 → 送压缩包 → 取解码画面

主要区别是初始化配置和画面输出位置:

环节 软件解码 D3D11 硬件解码
初始化 设置软件线程数和 FF_THREAD_FRAME 设置硬件设备与格式选择回调
输出画面 CPU 内存中的像素 GPU 解码表面
帧处理 转换为 NV12,再上传显示 保留 AVFrame 和设备引用
显示 采样上传后的 YUV 纹理 直接采样或通过 GPU 内部 YUV 复制准备纹理

最终由 shader 在绘制时完成 YUV 到 RGB 的颜色转换。

面试回答模板

项目的 AVC 线路使用 HTTP-FLV。我通过 FFmpeg 打开输入、探测流信息,取得包含 SPS/PPS 配置记录的流参数,再复制到解码上下文并打开解码器。

解封装线程持续读取压缩包,分发给音视频队列和缓存、录制旁路;视频线程使用 send/receive 接口解码,再根据时间戳同步并发布画面。AVCC 和 Annex-B 的解析由 FFmpeg 完成,应用使用统一接口。

正常起播由解码器建立参考状态;视频解码回退等恢复场景会等待关键包。seek 时清理队列、刷新解码状态并重置时钟。起播探测使用较小的 probesize 和 analyzeduration,播放积压通过队列和同步调度控制。

D3D11 硬解输出 GPU 表面,项目保留帧和设备引用,通过纹理采样或 GPU 内部 YUV 复制完成显示准备,最终由 shader 完成颜色转换。