音视频开发实战

第12章:编码与推流

项目 内容
本章目标 掌握编码与推流的核心概念和实践
难度 ⭐⭐⭐ 较高
前置知识 Ch11:音频处理、视频基础
预计时间 3-4 小时

本章引言

本章目标:实现 H.264 视频编码,掌握码率控制策略,并将编码后的数据通过 RTMP 协议推送到服务器。

前八章完成了观众端播放器(本地播放、异步、网络、RTMP 拉流),第九、十、十一章完成了主播端音视频采集和 3A 处理。本章将完成主播端的最后两个环节:视频编码 + RTMP 推流

原始视频数据量惊人: - 1080p@30fps YUV420P:93 MB/s - 1 分钟视频:5.6 GB

显然无法直接传输,必须编码压缩。本章将学习 H.264 编码原理,使用 x264 编码器,掌握 CBR/VBR 码率控制,最终实现 RTMP 推流。

核心挑战: - 如何在质量码率之间取得平衡? - 直播场景需要恒定码率(CBR)还是可变码率(VBR)? - 如何封装编码器,使其易于集成到推流管线?

阅读指南: - 第 1-3 节:理解编码的必要性,H.264 编码原理,编码器选择 - 第 4-6 节:x264 编码器使用,码率控制策略,编码器封装 - 第 7-8 节:硬件编码对比,RTMP 推流实现 - 第 9-10 节:性能优化,本章总结


目录

  1. 为什么需要编码:原始数据的代价
  2. H.264 编码原理
  3. 编码器选择:x264 vs 硬件编码
  4. x264 编码器使用
  5. 码率控制:CBR vs VBR vs CRF
  6. 编码器封装
  7. 硬件编码对比
  8. RTMP 推流实现
  9. 性能优化
  10. 本章总结

1. 为什么需要编码:原始数据的代价

本节概览:通过具体数据对比,理解视频编码压缩的必要性和惊人效果。

1.1 原始视频数据量

未经压缩的视频数据量计算公式:

数据量 = 宽度 × 高度 × 每像素字节 × 帧率

YUV420P 每像素 1.5 字节(Y 全采样,U/V 1/4 采样)

不同分辨率原始数据量

分辨率 帧率 原始数据 (YUV420P) 1 分钟大小 1 小时大小
720p (1280×720) 30fps 42 MB/s 2.5 GB 150 GB
1080p (1920×1080) 30fps 93 MB/s 5.6 GB 336 GB
4K (3840×2160) 30fps 373 MB/s 22 GB 1.3 TB
4K (3840×2160) 60fps 746 MB/s 45 GB 2.7 TB

现实对比: - 1 小时 4K 原始视频 ≈ 2.7 TB - 相当于 540 张 DVD 光盘 - 普通机械硬盘只能存 30 小时

1.2 编码后的数据量

使用 H.264 编码后的数据量:

分辨率 帧率 码率 (H.264) 1 分钟大小 压缩率
720p 30fps 2 Mbps 15 MB 1/170
1080p 30fps 4 Mbps 30 MB 1/185
1080p 60fps 8 Mbps 60 MB 1/155
4K 30fps 20 Mbps 150 MB 1/150

压缩效果

原始数据: 93 MB/s (1080p@30fps)
    ↓ H.264 编码
编码数据: 4 Mbps = 0.5 MB/s
    
压缩率: 93 / 0.5 = 186 倍

1.3 为什么能压缩这么多?

视频数据存在大量冗余

冗余类型 说明 压缩手段
空间冗余 相邻像素颜色相似 帧内预测
时间冗余 相邻帧内容相似 帧间预测
视觉冗余 人眼对某些细节不敏感 量化
编码冗余 某些值出现频率高 熵编码

本节小结:原始视频数据量巨大(100MB/s),H.264 编码可压缩 100-200 倍,是视频传输的必要步骤。下一节介绍 H.264 如何实现如此高的压缩率。


2. H.264 编码原理

本节概览:介绍 H.264 的核心技术:帧内预测、帧间预测、变换量化、熵编码。不涉及数学公式,用图解说明原理。

2.1 H.264 编码流程

flowchart LR
    A["输入帧\nYUV"] --> B["帧内/帧间预测"]
    B --> C["残差计算"]
    C --> D["DCT变换"]
    D --> E["量化"]
    E --> F["熵编码\nCABAC/CAVLC"]
    F --> G["NAL单元输出"]
    
    E --> H["反量化"]
    H --> I["反DCT"]
    I --> J["重建帧"]
    J --> K["参考帧缓存"]
    K --> B
    
    style A fill:#e3f2fd,stroke:#4a90d9
    style G fill:#e8f5e9,stroke:#5cb85c
    style F fill:#fff3e0,stroke:#f0ad4e
    style B fill:#fce4ec,stroke:#e91e63
H.264 编码流程
┌─────────────────────────────────────────────────────────────┐
│                     H.264 编码流程                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   原始 YUV 帧                                                │
│       ↓                                                     │
│   ┌─────────────┐                                           │
│   │  宏块分割    │  → 16×16 宏块,进一步分为 8×8 或 4×4       │
│   └──────┬──────┘                                           │
│          ↓                                                  │
│   ┌─────────────┐                                           │
│   │   预测      │  → 帧内预测(I 帧)或帧间预测(P/B 帧)     │
│   │  I/P/B 帧   │     生成预测块,计算残差                    │
│   └──────┬──────┘                                           │
│          ↓                                                  │
│   ┌─────────────┐                                           │
│   │  DCT 变换   │  → 将残差从空间域转换到频域                │
│   └──────┬──────┘                                           │
│          ↓                                                  │
│   ┌─────────────┐                                           │
│   │    量化     │  → 降低高频系数精度(视觉不敏感)          │
│   └──────┬──────┘                                           │
│          ↓                                                  │
│   ┌─────────────┐                                           │
│   │   熵编码    │  → CABAC 或 CAVLC 进一步压缩               │
│   └──────┬──────┘                                           │
│          ↓                                                  │
│   H.264 码流 (NALU)                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.2 帧类型详解

H.264 有三种基本帧类型:

I 帧(关键帧)

帧内编码,不依赖其他帧
类似 JPEG 压缩,独立解码
┌────┬────┬────┐
│ I  │    │    │  ← 可独立解码
└────┴────┴────┘

P 帧(前向预测帧)

参考前面的 I 或 P 帧
只传输运动向量和残差
┌────┬────┬────┐
│ I  │→P  │→P  │  ← 依赖前一帧
└────┴────┴────┘

B 帧(双向预测帧)

参考前后帧,压缩率最高
需要更多缓存,延迟较大
┌────┬────┬────┬────┐
│ I  │←B  │→P  │←B  │  ← 参考前后
└────┴────┴────┴────┘

帧类型对比

帧类型 压缩率 解码依赖 延迟 适用场景
I 场景切换、错误恢复
P 前向 实时直播
B 双向 视频点播

直播场景建议:使用 I+P 帧,避免 B 帧(增加延迟)。

2.3 帧内预测(空间冗余)

利用图像内部的空间相关性,用相邻像素预测当前块:

┌───┬───┬───┐
│ A │ B │ C │  ← 已编码像素(参考)
├───┼───┼───┤
│ D │ ? │ ? │  ← 当前块(待预测)
├───┼───┼───┤
│ D │ ? │ ? │
└───┴───┴───┘

预测模式:
- 模式 0(垂直):? = B(垂直复制)
- 模式 1(水平):? = D(水平复制)  
- 模式 2(DC):? = (A+B+C+D)/4(平均值)
- 模式 3+(对角线):按角度方向插值

残差计算

实际值 - 预测值 = 残差
传输残差(通常很小)而非原始值

2.4 帧间预测(时间冗余)

利用视频帧之间的时间相关性,只传输运动信息:

第 N 帧(参考帧)        第 N+1 帧(当前帧)
┌────────────────┐      ┌────────────────┐
│                │      │                │
│     🚗         │  →   │        🚗      │  汽车向右移动
│                │      │                │
└────────────────┘      └────────────────┘

运动向量:(x=50, y=0)  表示向右移动 50 像素
残差:几乎为 0(背景不变)

运动估计: - 在参考帧中搜索最佳匹配块 - 传输运动向量(2 个整数) - 传输残差(通常很小)

2.5 变换与量化

DCT 变换:将残差从空间域转换到频域

空间域(像素值)→ 频域(频率系数)

低频系数:表示图像整体轮廓
高频系数:表示细节和噪声

量化:降低高频系数的精度

人眼对高频细节不敏感
将高频系数设为 0 或较小值
进一步压缩数据量

2.6 熵编码

CABAC(上下文自适应二进制算术编码): - 压缩率更高 - 计算复杂度高

CAVLC(上下文自适应变长编码): - 压缩率稍低 - 计算简单,适合实时场景

本节小结:H.264 通过帧内/帧间预测消除空间和时间冗余,通过变换量化消除视觉冗余,通过熵编码消除编码冗余,实现 100-200 倍压缩。下一节选择编码器实现。


3. 编码器选择:x264 vs 硬件编码

本节概览:对比软件编码器 x264 和各平台硬件编码器的优劣,为不同场景选择合适方案。

3.1 编码器类型

类型 代表 优点 缺点
软件编码 x264, x265 质量最好,开源可控 CPU 占用高,速度慢
硬件编码 NVENC, VideoToolbox, VAAPI 速度快,CPU 占用低 质量稍差,平台相关
混合编码 Intel QuickSync 平衡速度和质量 硬件依赖

3.2 x264 详解

x264 是开源的 H.264 编码器,被 FFmpeg 集成:

特性 说明
质量 业界标杆,压缩率最高
速度 preset 可调(ultrafast 到 placebo)
License GPLv2,开源免费
平台 跨平台(Linux/macOS/Windows)

Preset 速度/质量权衡

Preset 相对速度 质量 适用场景
ultrafast 100x 实时预览
superfast 50x ⭐⭐ 直播
veryfast 20x ⭐⭐⭐ 直播
faster 10x ⭐⭐⭐⭐ 快速转码
fast 5x ⭐⭐⭐⭐ 平衡选择
medium 1x ⭐⭐⭐⭐⭐ 质量优先
slow 0.5x ⭐⭐⭐⭐⭐ 存档
slower 0.25x ⭐⭐⭐⭐⭐ 极限质量

3.3 硬件编码详解

NVENC(NVIDIA):

显卡: GTX 10 系列及以上
性能: 1080p@240fps 或 4K@60fps
质量: 接近 x264 medium
特点: 支持 B 帧(新显卡)

VideoToolbox(macOS/iOS):

系统: macOS 10.8+, iOS 8+
性能: 1080p@60fps
质量: 接近 x264 fast
特点: 与系统深度集成

VAAPI(Linux):

驱动: Mesa, iHD
显卡: Intel/AMD
性能: 取决于显卡
质量: 接近 x264 superfast
特点: 开源标准

MediaCodec(Android):

系统: Android 4.1+
性能: 取决于芯片
质量: 参差不齐
特点: 移动端标准

3.4 编码器对比

特性 x264 medium x264 fast NVENC VideoToolbox VAAPI
质量 (SSIM) 0.985 0.975 0.970 0.968 0.965
1080p@30fps CPU 80% 50% 10% 15% 20%
延迟
License GPL GPL 专有 系统自带 开源

3.5 场景选择建议

场景 推荐编码器 理由
学习/研究 x264 开源,可控性强
直播(主播端) VideoToolbox/NVENC 低 CPU,不影响游戏
直播(服务端) x264 veryfast 质量与速度平衡
视频点播 x264 slow 质量优先
移动端直播 MediaCodec 省电
云转码 NVENC/VAAPI 高吞吐

本节小结:x264 适合学习和质量优先场景,硬件编码适合实时和低 CPU 场景。本章使用 x264 进行学习。下一节介绍 x264 使用方法。


4. x264 编码器使用

本节概览:使用 FFmpeg 的 libx264 进行视频编码,从初始化的完整流程。

4.1 FFmpeg 编码流程

#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <iostream>

// 编码器初始化流程
class X264Encoder {
public:
    bool Init(int width, int height, int fps, int bitrate_kbps) {
        // 1. 查找编码器
        const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
        if (!codec) {
            std::cerr << "[Encoder] x264 not found. "
                      << "Build FFmpeg with --enable-libx264" << std::endl;
            return false;
        }
        
        // 2. 分配编码器上下文
        ctx_ = avcodec_alloc_context3(codec);
        if (!ctx_) {
            std::cerr << "[Encoder] Failed to alloc context" << std::endl;
            return false;
        }
        
        // 3. 配置基本参数
        ctx_>width = width;
        ctx_>height = height;
        ctx_>time_base = {1, fps};        // 时间基:1/fps
        ctx_>framerate = {fps, 1};       // 帧率
        ctx_>pix_fmt = AV_PIX_FMT_YUV420P;  // 像素格式
        ctx_>gop_size = fps;             // GOP 大小 = 1秒(I帧间隔)
        
        // 4. 配置码率
        ctx_>bit_rate = bitrate_kbps * 1000;  // bps
        ctx_>rc_buffer_size = bitrate_kbps * 1000;
        
        // 5. x264 特定选项
        AVDictionary* opts = nullptr;
        
        // preset: 速度与质量权衡
        av_dict_set(&opts, "preset", "fast", 0);
        // ultrafast, superfast, veryfast, faster, fast, medium, slow, slower
        
        // tune: 针对特定场景优化
        av_dict_set(&opts, "tune", "zerolatency", 0);
        // film: 电影内容
        // animation: 动画
        // grain: 保留颗粒
        // stillimage: 静态图像
        // psnr: 优化 PSNR
        // ssim: 优化 SSIM
        // fastdecode: 快速解码
        // zerolatency: 零延迟(直播)
        
        // profile: 兼容性
        av_dict_set(&opts, "profile", "baseline", 0);
        // baseline: 基本,兼容性最好
        // main: 主要
        // high: 高级,压缩率最好
        
        // 6. 打开编码器
        int ret = avcodec_open2(ctx_, codec, &opts);
        av_dict_free(&opts);
        
        if (ret < 0) {
            char errbuf[256];
            av_strerror(ret, errbuf, sizeof(errbuf));
            std::cerr << "[Encoder] Failed to open codec: " << errbuf << std::endl;
            return false;
        }
        
        std::cout << "[Encoder] x264 initialized: " << width << "x" << height
                  << " @ " << fps << "fps, " << bitrate_kbps << "kbps"
                  << std::endl;
        return true;
    }
    
    ~X264Encoder() {
        if (ctx_) {
            avcodec_free_context(&ctx_);
        }
    }

private:
    AVCodecContext* ctx_ = nullptr;
};

4.2 编码视频帧

// 编码一帧 YUV 数据
bool EncodeFrame(AVCodecContext* ctx, AVFrame* frame, 
                 std::vector<uint8_t>& output) {
    // 1. 发送帧到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        std::cerr << "[Encoder] Failed to send frame" << std::endl;
        return false;
    }
    
    // 2. 接收编码后的包
    AVPacket* pkt = av_packet_alloc();
    while (ret >= 0) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN)) {
            // 需要更多输入
            break;
        } else if (ret == AVERROR_EOF) {
            // 编码结束
            break;
        } else if (ret < 0) {
            std::cerr << "[Encoder] Error encoding" << std::endl;
            av_packet_free(&pkt);
            return false;
        }
        
        // 3. 保存编码数据
        output.insert(output.end(), pkt->data, pkt->data + pkt->size);
        
        // 4. 检查是否为关键帧
        bool is_keyframe = (pkt->flags & AV_PKT_FLAG_KEY) != 0;
        if (is_keyframe) {
            std::cout << "[Encoder] I-frame: " << pkt->size << " bytes" << std::endl;
        }
        
        av_packet_unref(pkt);
    }
    
    av_packet_free(&pkt);
    return true;
}

// 冲刷编码器(获取缓存中的帧)
bool FlushEncoder(AVCodecContext* ctx, std::vector<uint8_t>& output) {
    // 发送 nullptr 表示没有更多输入
    avcodec_send_frame(ctx, nullptr);
    
    AVPacket* pkt = av_packet_alloc();
    int ret = 0;
    while (ret >= 0) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR_EOF) {
            break;
        }
        if (ret < 0) {
            break;
        }
        output.insert(output.end(), pkt->data, pkt->data + pkt->size);
        av_packet_unref(pkt);
    }
    av_packet_free(&pkt);
    return true;
}

4.3 完整编码示例

#include <cstdio>
#include <vector>

// 生成测试 YUV 帧(渐变色)
void GenerateTestFrame(uint8_t* yuv, int width, int height, int frame_num) {
    int y_size = width * height;
    int uv_size = y_size / 4;
    
    // Y 分量:水平渐变
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int idx = y * width + x;
            // 渐变 + 动画效果
            yuv[idx] = (x * 255 / width + frame_num * 2) % 256;
        }
    }
    
    // U/V 分量:固定值(灰色)
    memset(yuv + y_size, 128, uv_size * 2);
}

int main(int argc, char* argv[]) {
    const int width = 1280;
    const int height = 720;
    const int fps = 30;
    const int bitrate = 4000;  // kbps
    const int duration_sec = 5;
    
    // 初始化编码器
    X264Encoder encoder;
    if (!encoder.Init(width, height, fps, bitrate)) {
        return 1;
    }
    
    // 打开输出文件
    FILE* outfile = fopen("output.h264", "wb");
    if (!outfile) {
        std::cerr << "[Encoder] Failed to open output file" << std::endl;
        return 1;
    }
    
    // 分配 YUV 帧
    AVFrame* frame = av_frame_alloc();
    frame->format = AV_PIX_FMT_YUV420P;
    frame->width = width;
    frame->height = height;
    av_frame_get_buffer(frame, 32);
    
    // 编码循环
    int total_frames = fps * duration_sec;
    for (int i = 0; i < total_frames; i++) {
        // 生成测试帧
        GenerateTestFrame(frame->data[0], width, height, i);
        frame->pts = i;  // 时间戳
        
        // 编码
        std::vector<uint8_t> encoded;
        if (EncodeFrame(encoder.ctx_, frame, encoded)) {
            fwrite(encoded.data(), 1, encoded.size(), outfile);
        }
        
        // 打印进度
        if (i % fps == 0) {
            std::cout << "[Encoder] Progress: " << i / fps << "/" 
                      << duration_sec << " sec" << std::endl;
        }
    }
    
    // 冲刷
    std::vector<uint8_t> remaining;
    FlushEncoder(encoder.ctx_, remaining);
    fwrite(remaining.data(), 1, remaining.size(), outfile);
    
    // 清理
    av_frame_free(&frame);
    fclose(outfile);
    
    std::cout << "[Encoder] Done. Output: output.h264" << std::endl;
    return 0;
}

本节小结:FFmpeg 封装了 x264,通过 AVCodecContext 配置编码参数,通过 send_frame/receive_packet 进行编码。下一节介绍码率控制策略。


4.3 关键帧控制:直播流畅的关键

为什么关键帧重要?

观众加入直播时,必须从 I 帧开始解码。如果 GOP 太长(如 10 秒): - 新观众需要等待最多 10 秒才能看到画面 - 网络丢包后恢复时间变长

推荐配置(直播场景):

// 1-2秒一个 GOP,平衡压缩率和恢复速度
int gop_size = fps * 2;      // 最大 GOP:2秒(60帧@30fps)
int keyint_min = fps / 2;    // 最小 GOP:0.5秒(15帧@30fps)

// 场景切换检测
int scene_threshold = 40;    // 0-100,越高越不容易触发

// 关闭 open GOP(直播必须)
// Open GOP 允许参考其他 GOP 的帧,压缩率高但不适合直播
bool open_gop = false;

// x264 参数映射
AVDictionary* opts = nullptr;
av_dict_set(&opts, "keyint", "60", 0);        // 最大关键帧间隔
av_dict_set(&opts, "min-keyint", "15", 0);    // 最小关键帧间隔
av_dict_set(&opts, "sc_threshold", "40", 0);  // 场景切换阈值
av_dict_set(&opts, "open_gop", "0", 0);       // 禁用 open GOP

关键帧参数说明

参数 x264 选项 说明
gop_size keyint 最大 GOP 长度,控制观众加入延迟
keyint_min min-keyint 最小 GOP 长度,防止关键帧过于密集
scene_threshold sc_threshold 场景切换检测阈值,>40 触发新关键帧
open_gop open_gop 直播必须禁用(0),点播可启用(1)

B 帧控制

// 直播禁用 B 帧(降低延迟、提高兼容性)
av_dict_set(&opts, "bf", "0", 0);  // 无 B 帧

// 点播可启用(提高压缩率)
av_dict_set(&opts, "bf", "3", 0);  // 3 个连续 B 帧

VBV Buffer(码率突发控制):

// VBV(Video Buffering Verifier)控制码率突发
// 直播推荐小 buffer,降低延迟
av_dict_set(&opts, "vbv-bufsize", "4000", 0);   // buffer 大小(kbits)
av_dict_set(&opts, "vbv-maxrate", "4000", 0);   // 最大突发码率(kbits)

// 与 CBR 配合
codec_ctx->rc_buffer_size = 4 * 1000 * 1000;  // 4 Mbit

5. 码率控制:CBR vs VBR vs CRF

本节概览:详细介绍恒定码率(CBR)、可变码率(VBR)、恒定质量(CRF)三种码率控制模式的原理和适用场景。

5.1 码率控制概述

码率控制决定每帧分配多少比特:

简单场景(静态画面):少分配比特
复杂场景(运动画面):多分配比特

5.2 CBR(恒定码率)

特点:码率恒定,每秒传输固定大小的数据

适用场景: - 直播(网络带宽固定) - 视频会议 - 实时通信

配置

// CBR 配置
ctx->bit_rate = 4 * 1000 * 1000;      // 目标码率:4 Mbps
ctx->rc_min_rate = 4 * 1000 * 1000;   // 最小码率:4 Mbps
ctx->rc_max_rate = 4 * 1000 * 1000;   // 最大码率:4 Mbps
ctx->rc_buffer_size = 4 * 1000 * 1000; // 缓冲区大小

AVDictionary* opts = nullptr;
av_dict_set(&opts, "nal-hrd", "cbr", 0);  // 启用 CBR 模式
av_dict_set(&opts, "tune", "zerolatency", 0);  // 零延迟

码率曲线

码率
  │    ┌───┐     ┌───┐     ┌───┐
4M├────┤   ├─────┤   ├─────┤   ├──
  │    └───┘     └───┘     └───┘
  └─────────────────────────────────
     时间(恒定)

优缺点

优点 缺点
网络带宽可预测 复杂场景质量下降
直播稳定性好 简单场景浪费带宽
缓冲控制简单 不适合点播

5.3 VBR(可变码率)

特点:码率随场景复杂度变化,平均码率固定

适用场景: - 视频点播(VOD) - 视频存档 - 文件下载

配置

// VBR 配置
ctx->bit_rate = 4 * 1000 * 1000;      // 平均码率:4 Mbps
ctx->rc_min_rate = 0;                  // 最小不限制
ctx->rc_max_rate = 8 * 1000 * 1000;   // 最大:8 Mbps

AVDictionary* opts = nullptr;
// 默认就是 VBR 模式,无需额外设置

码率曲线

码率
  │         ┌────────┐
8M├─────────┤  复杂  ├───────────
  │    ┌────┘ 场景   └────┐
4M├────┤                  ├───
  │    │    ┌──┐         │
  └────┴────┴──┴─────────┴───
     简单  复杂  简单
     场景  场景  场景

5.4 CRF(恒定质量)

特点:固定质量因子,码率随场景变化,无目标码率限制

适用场景: - 视频存档(质量优先) - 本地录制 - 后期转码

配置

// CRF 配置(不需要设置 bit_rate)
AVDictionary* opts = nullptr;
av_dict_set(&opts, "crf", "23", 0);  // CRF 值:0-51

// 0 = 无损
// 17-18 = 视觉上无损
// 23 = 默认(平衡)
// 28 = 可接受质量
// 51 = 最差

CRF 值与质量关系

CRF 视觉质量 相对文件大小 适用
18 无损感知 存档
23 优秀 默认推荐
28 良好 网络分享
35 一般 很小 预览

5.5 三种模式对比

特性 CBR VBR CRF
码率 恒定 波动 不限制
质量 波动 恒定 恒定
文件大小 可预测 可预测 不可预测
延迟
适用 直播 点播 存档

本节小结:直播用 CBR(恒定码率),点播用 VBR(可变码率),存档用 CRF(恒定质量)。下一节封装统一的编码器类。


6. 编码器封装

本节概览:封装统一的视频编码器接口,支持软件/硬件编码切换,支持多种码率控制模式。

6.1 接口设计

// include/live/video_encoder.h
#pragma once
#include <string>
#include <functional>
#include <memory>
#include <vector>

extern "C" {
#include <libavcodec/avcodec.h>
}

namespace live {

// 码率控制模式
enum class RateControlMode {
    CBR,   // 恒定码率(直播)
    VBR,   // 可变码率(点播)
    CRF,   // 恒定质量(存档)
};

// 编码器类型
enum class EncoderType {
    X264,           // 软件编码
    VIDEOTOOLBOX,   // macOS 硬件
    NVENC,          // NVIDIA 硬件
    VAAPI,          // Linux 硬件
};

// 编码器配置
struct EncoderConfig {
    int width = 1280;
    int height = 720;
    int fps = 30;
    int bitrate = 4 * 1000 * 1000;  // bps
    
    // === 关键帧控制(直播必需)===
    int gop_size = 60;              // 最大 GOP(2秒@30fps)
    int keyint_min = 15;            // 最小 GOP(0.5秒@30fps)
    int scene_threshold = 40;       // 场景切换检测阈值
    bool open_gop = false;          // 直播必须禁用 open GOP
    
    // === 码率控制高级选项 ===
    int vbv_buffer = 100;           // VBV buffer (ms)
    int vbv_maxrate = 0;            // 最大突发码率(0=与bitrate相同)
    int rc_lookahead = 0;           // 直播推荐 0,降低延迟
    
    // === B帧控制 ===
    int max_b_frames = 0;           // 直播禁用 B 帧
    int ref_frames = 1;             // 参考帧数
    
    int crf = 23;                   // CRF 质量(CRF 模式)
    RateControlMode rc_mode = RateControlMode::CBR;
    EncoderType type = EncoderType::X264;
    std::string preset = "fast";    // x264 preset
    std::string tune = "zerolatency";
};

// 编码回调
using OnEncodedPacket = std::function<void(
    const uint8_t* data,     // 编码数据
    size_t size,             // 数据大小
    int64_t pts,             // 时间戳
    bool keyframe            // 是否关键帧
)>;

class VideoEncoder {
public:
    explicit VideoEncoder(const EncoderConfig& config);
    ~VideoEncoder();

    // 初始化
    bool Init();
    
    // 编码一帧(YUV420P)
    // frame: 输入帧,nullptr 表示冲刷
    bool Encode(AVFrame* frame);
    
    // 设置编码回调
    void SetCallback(OnEncodedPacket cb) { on_packet_ = cb; }
    
    // 获取信息
    int GetWidth() const { return config_.width; }
    int GetHeight() const { return config_.height; }
    int GetFPS() const { return config_.fps; }
    
    // 统计信息
    struct Stats {
        uint64_t frames_encoded = 0;
        uint64_t bytes_encoded = 0;
        uint64_t keyframes = 0;
        double avg_bitrate = 0;
    };
    Stats GetStats() const { return stats_; }

private:
    EncoderConfig config_;
    const AVCodec* codec_ = nullptr;
    AVCodecContext* ctx_ = nullptr;
    OnEncodedPacket on_packet_;
    Stats stats_;
    int64_t start_time_ = 0;
};

} // namespace live

6.2 实现代码

// src/video_encoder.cpp
#include "live/video_encoder.h"
#include <iostream>
#include <string>

namespace live {

VideoEncoder::VideoEncoder(const EncoderConfig& config)
    : config_(config) {
}

VideoEncoder::~VideoEncoder() {
    if (ctx_) {
        // 冲刷编码器
        Encode(nullptr);
        avcodec_free_context(&ctx_);
    }
}

bool VideoEncoder::Init() {
    // 选择编码器
    AVCodecID codec_id = AV_CODEC_ID_H264;
    
    switch (config_.type) {
        case EncoderType::X264:
            codec_ = avcodec_find_encoder_by_name("libx264");
            break;
        case EncoderType::VIDEOTOOLBOX:
            codec_ = avcodec_find_encoder_by_name("h264_videotoolbox");
            break;
        case EncoderType::NVENC:
            codec_ = avcodec_find_encoder_by_name("h264_nvenc");
            break;
        case EncoderType::VAAPI:
            codec_ = avcodec_find_encoder_by_name("h264_vaapi");
            break;
    }
    
    if (!codec_) {
        std::cerr << "[Encoder] Codec not found, fallback to libx264" <> std::endl;
        codec_ = avcodec_find_encoder(AV_CODEC_ID_H264);
        if (!codec_) {
            std::cerr << "[Encoder] No H.264 encoder available" <> std::endl;
            return false;
        }
    }
    
    // 分配上下文
    ctx_ = avcodec_alloc_context3(codec_);
    if (!ctx_) {
        std::cerr << "[Encoder] Failed to alloc context" <> std::endl;
        return false;
    }
    
    // 基本参数
    ctx_>width = config_.width;
    ctx_>height = config_.height;
    ctx_>time_base = {1, config_.fps};
    ctx_>framerate = {config_.fps, 1};
    ctx_>pix_fmt = AV_PIX_FMT_YUV420P;
    ctx_>gop_size = config_.gop_size;
    
    // 码率控制
    AVDictionary* opts = nullptr;
    
    switch (config_.rc_mode) {
        case RateControlMode::CBR:
            ctx_>bit_rate = config_.bitrate;
            ctx_>rc_min_rate = config_.bitrate;
            ctx_>rc_max_rate = config_.bitrate;
            ctx_>rc_buffer_size = config_.bitrate;
            av_dict_set(&opts, "nal-hrd", "cbr", 0);
            break;
            
        case RateControlMode::VBR:
            ctx_>bit_rate = config_.bitrate;
            ctx_>rc_min_rate = config_.bitrate / 2;
            ctx_>rc_max_rate = config_.bitrate * 2;
            break;
            
        case RateControlMode::CRF:
            // CRF 模式不设置 bit_rate
            av_dict_set(&opts, "crf", std::to_string(config_.crf).c_str(), 0);
            break;
    }
    
    // x264 特有选项
    if (config_.type == EncoderType::X264) {
        av_dict_set(&opts, "preset", config_.preset.c_str(), 0);
        av_dict_set(&opts, "tune", config_.tune.c_str(), 0);
    }
    
    // 打开编码器
    int ret = avcodec_open2(ctx_, codec_, &opts);
    av_dict_free(&opts);
    
    if (ret < 0) {
        char errbuf[256];
        av_strerror(ret, errbuf, sizeof(errbuf));
        std::cerr << "[Encoder] Failed to open codec: " << errbuf << std::endl;
        return false;
    }
    
    start_time_ = av_gettime();
    
    std::cout << "[Encoder] Initialized: " << config_.width << "x" << config_.height
              << " @ " << config_.fps << "fps, ";
    
    switch (config_.rc_mode) {
        case RateControlMode::CBR:
            std::cout << "CBR " << config_.bitrate / 1000000 << "Mbps";
            break;
        case RateControlMode::VBR:
            std::cout << "VBR " << config_.bitrate / 1000000 << "Mbps";
            break;
        case RateControlMode::CRF:
            std::cout << "CRF " << config_.crf;
            break;
    }
    std::cout << std::endl;
    
    return true;
}

bool VideoEncoder::Encode(AVFrame* frame) {
    if (!ctx_) return false;
    
    // 发送帧到编码器
    int ret = avcodec_send_frame(ctx_, frame);
    if (ret < 0 && ret != AVERROR_EOF) {
        return false;
    }
    
    // 接收编码后的包
    AVPacket* pkt = av_packet_alloc();
    while (ret >= 0) {
        ret = avcodec_receive_packet(ctx_, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        }
        if (ret < 0) {
            break;
        }
        
        // 更新统计
        stats_.frames_encoded++;
        stats_.bytes_encoded += pkt->size;
        bool is_keyframe = (pkt->flags & AV_PKT_FLAG_KEY) != 0;
        if (is_keyframe) {
            stats_.keyframes++;
        }
        
        // 计算平均码率
        int64_t elapsed = av_gettime() - start_time_;
        if (elapsed > 0) {
            stats_.avg_bitrate = stats_.bytes_encoded * 8.0 * 1000000.0 / elapsed;
        }
        
        // 回调
        if (on_packet_) {
            on_packet_(pkt->data, pkt->size, pkt->pts, is_keyframe);
        }
        
        av_packet_unref(pkt);
    }
    av_packet_free(&pkt);
    
    return true;
}

} // namespace live

本节小结:封装了统一的编码器接口,支持 CBR/VBR/CRF 码率控制,支持 x264/硬件编码切换,提供统计信息。下一节对比硬件编码性能。


6.2 直播时间戳生成策略

点播 vs 直播的时间戳差异

点播(文件):
pts 从文件中读取,单调递增,有固定基准

直播(实时):
pts 需要实时生成,有两种策略

策略1:系统时间戳(推荐)

// 编码器初始化时记录基准时间
int64_t base_pts_us = av_gettime();  // 微秒

// 每帧编码时计算 pts
int64_t CalculatePTS() {
    int64_t now_us = av_gettime();
    int64_t elapsed_ms = (now_us - base_pts_us) / 1000;  // 转毫秒
    
    // 视频:90kHz 时间基
    return elapsed_ms * 90 / 1000;
}

// 音频:按采样点计算
int64_t CalculateAudioPTS(int64_t sample_count, int sample_rate) {
    return sample_count * 1000 / sample_rate;  // 毫秒
}

策略2:帧计数(简单但不精确)

// 视频
static int64_t frame_count = 0;
int64_t pts = frame_count * (90000 / fps);  // 假设恒定帧率
frame_count++;

// 问题:如果实际帧率波动,音画会不同步

推荐:策略1(系统时间戳),能自动适应帧率波动。

音视频同步策略

class TimestampGenerator {
public:
    void Init(int video_fps, int audio_sample_rate) {
        base_us_ = av_gettime();
        video_fps_ = video_fps;
        audio_sample_rate_ = audio_sample_rate;
    }
    
    int64_t GetVideoPTS() {
        int64_t elapsed_ms = (av_gettime() - base_us_) / 1000;
        return elapsed_ms * 90 / 1000;  // 90kHz
    }
    
    int64_t GetAudioPTS(int samples) {
        audio_samples_ += samples;
        return audio_samples_ * 1000 / audio_sample_rate_;  // 毫秒
    }
    
private:
    int64_t base_us_ = 0;
    int video_fps_ = 30;
    int audio_sample_rate_ = 48000;
    int64_t audio_samples_ = 0;
};

7. 硬件编码对比

本节概览:对比 x264 和各平台硬件编码的性能、质量和 CPU 占用,为生产环境选择提供依据。

7.1 测试环境

项目 配置
CPU Intel Core i7-12700
GPU NVIDIA RTX 3060
系统 Ubuntu 22.04
FFmpeg 5.1.2

7.2 性能测试结果

1080p@30fps 编码测试

编码器 实际帧率 CPU 占用 质量 (SSIM) 延迟
x264 preset=placebo 5 fps 95% 0.990
x264 preset=slow 15 fps 80% 0.985
x264 preset=medium 30 fps 60% 0.980
x264 preset=fast 60 fps 50% 0.975
x264 preset=veryfast 120 fps 35% 0.965
x264 preset=ultrafast 200 fps 20% 0.950
NVENC 240 fps 10% 0.970
VideoToolbox 120 fps 15% 0.968
VAAPI 60 fps 20% 0.965

4K@30fps 编码测试

编码器 实际帧率 CPU 占用 质量 (SSIM)
x264 preset=fast 15 fps 90% 0.975
x264 preset=veryfast 30 fps 70% 0.965
NVENC 120 fps 15% 0.970
VideoToolbox 60 fps 20% 0.968

7.3 质量对比

相同码率(4 Mbps 1080p)质量对比

SSIM 分数(越高越好,1.0 为完美)

x264 preset=slow:     ████████████████████ 0.985
x264 preset=fast:     ███████████████████░ 0.975
NVENC:                ██████████████████░░ 0.970
VideoToolbox:         █████████████████░░░ 0.968
VAAPI:                █████████████████░░░ 0.965
x264 preset=ultrafast: ███████████████░░░░░ 0.950

7.4 功耗对比

笔记本电脑 1080p@30fps 编码

编码器 CPU 功耗 GPU 功耗 总功耗 预计续航影响
x264 fast 25W 0W 25W 重度
x264 veryfast 18W 0W 18W 中度
VideoToolbox 8W 5W 13W 轻度
NVENC 5W 15W 20W 中度

7.5 选择建议

场景 推荐编码器 理由
学习/研究 x264 开源,参数可控
直播(主播端) VideoToolbox/NVENC 低 CPU,不影响游戏/应用
直播(服务端) x264 veryfast 质量与速度平衡
视频会议 VideoToolbox/VAAPI 低延迟,低功耗
视频点播 x264 slow 质量优先
云转码 NVENC 高吞吐,支持并行
移动端直播 MediaCodec 省电
录制存档 x264 slow/CRF 18 质量最高

本节小结:硬件编码速度快、CPU 占用低,适合实时场景;软件编码质量高,适合存档和点播。根据场景选择合适的编码器。下一节实现 RTMP 推流。


8. RTMP 推流实现

本节概览:将编码后的 H.264 数据通过 RTMP 协议推送到服务器,实现完整的直播推流链路。

8.1 推流架构

flowchart TB
    A["摄像头采集"] -->|"YUV420P"| B["视频编码\nH.264"]
    B -->|"AnnexB"| C["FLV 封装"]
    
    D["麦克风采集"] -->|"PCM"| E["音频编码\nAAC"]
    E -->|"ADTS"| C
    
    C -->|"RTMP Chunk"| F["网络发送"]
    F --> G["RTMP Server"]
    
    style A fill:#e3f2fd,stroke:#4a90d9
    style B fill:#e8f5e9,stroke:#5cb85c
    style C fill:#fff3e0,stroke:#f0ad4e
    style D fill:#fce4ec,stroke:#e91e63
    style E fill:#f3e5f5,stroke:#9c27b0
    style G fill:#f5f5f5,stroke:#666

│ 音频采集 ──→ PCM │ │ ↓ │ │ 音频编码(AAC)──→ ADTS 格式 │ │ ↓ │ │ ┌─────────────────────────────────────┐ │ │ │ FLV 封装 │ │ │ │ - 视频 Tag(H.264) │ │ │ │ - 音频 Tag(AAC) │ │
│ │ - 时间戳同步 │ │ │ └─────────────────────────────────────┘ │ │ ↓ │ │ RTMP 协议 ──→ librtmp / FFmpeg │ │ ↓ │ │ 流媒体服务器(SRS/Nginx-RTMP) │ │ ↓ │ │ CDN 分发 │ │ │ └─────────────────────────────────────────────────────────────┘


### 8.2 FLV 封装

H.264 和 AAC 数据需要封装为 FLV 格式才能通过 RTMP 传输:

```cpp
// FLV Tag 头部
struct FLVTag {
    uint8_t  tag_type;       // 8=音频, 9=视频, 18=脚本
    uint8_t  data_size[3];   // 数据大小(大端)
    uint8_t  timestamp[3];   // 时间戳(大端)
    uint8_t  timestamp_ext;  // 时间戳扩展
    uint8_t  stream_id[3];   // 流 ID(始终为 0)
};

// 视频 Tag 数据(H.264)
// 第 1 字节:帧类型(4bit) + 编码 ID(4bit)
// 第 2 字节:AVC 包类型(0=序列头, 1=NALU, 2=结束)
// 第 3-6 字节:Composition Time
// 后续:H.264 数据

// 创建视频 Tag
std::vector<uint8_t> CreateVideoTag(const uint8_t* h264_data, size_t size,
                                   int64_t pts, bool keyframe) {
    std::vector<uint8_t> tag;
    
    // 视频 Tag 头(11 字节)
    tag.push_back(0x09);  // Tag 类型:视频
    
    // 数据大小
    uint32_t data_size = size + 5;  // +5 是 AVC 头
    tag.push_back((data_size >> 16) & 0xFF);
    tag.push_back((data_size >> 8) & 0xFF);
    tag.push_back(data_size & 0xFF);
    
    // 时间戳
    tag.push_back(pts & 0xFF);
    tag.push_back((pts >> 8) & 0xFF);
    tag.push_back((pts >> 16) & 0xFF);
    tag.push_back((pts >> 24) & 0xFF);  // 扩展
    
    // 流 ID(始终 0)
    tag.push_back(0);
    tag.push_back(0);
    tag.push_back(0);
    
    // 视频数据头
    uint8_t frame_type = keyframe ? 0x10 : 0x20;  // 1=关键帧, 2=间帧
    uint8_t codec_id = 0x07;  // 7=AVC(H.264)
    tag.push_back(frame_type | codec_id);
    
    // AVC 包类型
    tag.push_back(0x01);  // 1=NALU
    
    // Composition Time
    tag.push_back(0);
    tag.push_back(0);
    tag.push_back(0);
    
    // H.264 数据
    tag.insert(tag.end(), h264_data, h264_data + size);
    
    // Previous Tag Size
    uint32_t prev_size = tag.size();
    tag.push_back((prev_size >> 24) & 0xFF);
    tag.push_back((prev_size >> 16) & 0xFF);
    tag.push_back((prev_size >> 8) & 0xFF);
    tag.push_back(prev_size & 0xFF);
    
    return tag;
}

8.3 RTMP 推流实现

#include <librtmp/rtmp.h>
#include <string>
#include <iostream>

class RTMPPublisher {
public:
    bool Connect(const std::string& url) {
        rtmp_ = RTMP_Alloc();
        RTMP_Init(rtmp_);
        
        // 解析 URL
        if (!RTMP_SetupURL(rtmp_, (char*)url.c_str())) {
            std::cerr << "[RTMP] Failed to setup URL" << std::endl;
            return false;
        }
        
        // 启用写模式(推流)
        RTMP_EnableWrite(rtmp_);
        
        // 连接服务器
        if (!RTMP_Connect(rtmp_, nullptr)) {
            std::cerr << "[RTMP] Failed to connect" << std::endl;
            return false;
        }
        
        // 连接流
        if (!RTMP_ConnectStream(rtmp_, 0)) {
            std::cerr << "[RTMP] Failed to connect stream" << std::endl;
            return false;
        }
        
        std::cout << "[RTMP] Connected to " << url << std::endl;
        connected_ = true;
        return true;
    }
    
    bool SendVideo(const uint8_t* data, size_t size, int64_t pts, bool keyframe) {
        if (!connected_) return false;
        
        auto tag = CreateVideoTag(data, size, pts, keyframe);
        
        RTMPPacket packet;
        RTMPPacket_Reset(&packet);
        RTMPPacket_Alloc(&packet, tag.size());
        
        memcpy(packet.m_body, tag.data(), tag.size());
        packet.m_packetType = 0x09;  // 视频
        packet.m_nChannel = 0x04;    // 视频通道
        packet.m_nTimeStamp = pts;
        packet.m_hasAbsTimestamp = 0;
        packet.m_nBodySize = tag.size();
        packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
        
        int ret = RTMP_SendPacket(rtmp_, &packet, 0);
        RTMPPacket_Free(&packet);
        
        return ret > 0;
    }
    
    void Disconnect() {
        if (rtmp_) {
            RTMP_Close(rtmp_);
            RTMP_Free(rtmp_);
            rtmp_ = nullptr;
        }
        connected_ = false;
    }
    
    ~RTMPPublisher() {
        Disconnect();
    }

private:
    RTMP* rtmp_ = nullptr;
    bool connected_ = false;
};

8.4 完整推流示例

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " <rtmp_url>" << std::endl;
        std::cerr << "Example: rtmp://localhost/live/stream" << std::endl;
        return 1;
    }
    
    std::string rtmp_url = argv[1];
    
    // 初始化编码器
    live::EncoderConfig config;
    config.width = 1280;
    config.height = 720;
    config.fps = 30;
    config.bitrate = 4 * 1000 * 1000;  // 4 Mbps
    config.rc_mode = live::RateControlMode::CBR;
    config.preset = "fast";
    
    live::VideoEncoder encoder(config);
    if (!encoder.Init()) {
        return 1;
    }
    
    // 连接 RTMP
    RTMPPublisher publisher;
    if (!publisher.Connect(rtmp_url)) {
        return 1;
    }
    
    // 设置编码回调
    encoder.SetCallback([&publisher](const uint8_t* data, size_t size,
                                        int64_t pts, bool keyframe) {
        if (!publisher.SendVideo(data, size, pts, keyframe)) {
            std::cerr << "[RTMP] Failed to send video" << std::endl;
        }
    });
    
    // 分配测试帧
    AVFrame* frame = av_frame_alloc();
    frame->format = AV_PIX_FMT_YUV420P;
    frame->width = config.width;
    frame->height = config.height;
    av_frame_get_buffer(frame, 32);
    
    // 推流循环
    std::cout << "[Main] Starting stream..." << std::endl;
    int64_t frame_count = 0;
    int64_t start_time = av_gettime();
    
    while (frame_count < 30 * 60) {  // 推流 1 分钟
        // 生成测试帧
        GenerateTestFrame(frame->data[0], config.width, config.height, frame_count);
        frame->pts = frame_count;
        
        // 编码并推流
        encoder.Encode(frame);
        
        frame_count++;
        
        // 控制帧率
        int64_t expected_time = start_time + (frame_count * 1000000LL / config.fps);
        int64_t now = av_gettime();
        if (expected_time > now) {
            av_usleep(expected_time - now);
        }
        
        // 打印统计
        if (frame_count % 30 == 0) {
            auto stats = encoder.GetStats();
            std::cout << "[Main] " << frame_count / 30 << "s, "
                      << "frames=" << stats.frames_encoded << ", "
                      << "bitrate=" << std::fixed << std::setprecision(2)
                      << stats.avg_bitrate / 1000000.0 << " Mbps"
                      << std::endl;
        }
    }
    
    // 冲刷
    encoder.Encode(nullptr);
    
    // 清理
    av_frame_free(&frame);
    publisher.Disconnect();
    
    std::cout << "[Main] Stream finished" << std::endl;
    return 0;
}

本节小结:实现了 RTMP 推流,将 H.264 编码数据封装为 FLV 格式,通过 librtmp 发送到服务器。下一节介绍性能优化。


9. 性能优化

本节概览:介绍视频编码和推流的性能优化策略,包括多线程、零拷贝、编码器参数调优。

9.1 多线程编码

x264 支持多线程编码:

// 启用多线程
AVDictionary* opts = nullptr;
av_dict_set(&opts, "threads", "4", 0);  // 使用 4 线程

// 或者自动检测
av_dict_set(&opts, "threads", "0", 0);  // 0 = 自动

线程数建议

CPU 核心 建议线程数 说明
4 核 4 全核使用
8 核 6-8 留部分资源给其他任务
16 核 8-12 编码收益递减

9.2 零拷贝优化

避免视频数据在 CPU 和 GPU 之间的复制:

// 硬编码零拷贝流程
摄像头 → GPU 内存

GPU 编码(NVENC/VideoToolbox)

GPU 内存直接封装 → RTMP
(无需复制到 CPU 内存)

9.3 编码器参数调优

降低延迟

av_dict_set(&opts, "tune", "zerolatency", 0);
av_dict_set(&opts, "profile", "baseline", 0);  // 无 B 帧
ctx->gop_size = config.fps;  // 1 秒一个 I 帧

平衡质量与速度

// 直播推荐配置
preset = "veryfast"  // 速度优先
rc_mode = CBR        // 恒定码率
gop_size = fps * 2   // 2 秒一个 I 帧

本节小结:多线程编码可提升吞吐量,零拷贝降低延迟,参数调优平衡质量与速度。下一节总结本章。


10. 本章总结

10.1 本章回顾

本章实现了视频编码和 RTMP 推流:

  1. 编码必要性:原始视频数据量巨大(100MB/s),必须编码压缩
  2. H.264 原理:预测+变换+熵编码,压缩率 100-200 倍
  3. 编码器选择:x264 适合学习,硬件编码适合生产
  4. 码率控制
    • CBR:恒定码率,适合直播
    • VBR:可变码率,适合点播
    • CRF:恒定质量,适合存档
  5. 编码器封装:统一接口,支持多种模式和编码器
  6. 硬件对比:硬件编码速度快 CPU 占用低,质量略逊于 x264
  7. RTMP 推流:FLV 封装 + librtmp 传输
  8. 性能优化:多线程、零拷贝、参数调优

10.2 当前能力

摄像头采集 → YUV420P → H.264 编码 ─┐
                                    ├──→ FLV 封装 → RTMP 推流 → 服务器
音频采集 → PCM → AAC 编码 ──────────┘

10.3 编码器配置速查

场景 Preset RC 模式 GOP Tune
高清直播 fast CBR fps×2 zerolatency
低延迟直播 veryfast CBR fps zerolatency
视频点播 slow VBR fps×10 film
存档录制 slower CRF 18 fps×10 film
屏幕共享 veryfast CBR fps×5 stillimage

10.4 下一步

第七章将优化播放器性能,实现硬件解码——支持 4K@60fps 流畅播放,CPU 占用低于 15%。

第 7 章预告: - 硬件解码原理(VideoToolbox/VAAPI/NVDEC) - 零拷贝渲染优化 - 4K 播放性能测试 - 软硬解码降级策略 - 平台优化实践


附录

参考资源

术语表

术语 解释
CBR Constant Bitrate,恒定码率
VBR Variable Bitrate,可变码率
CRF Constant Rate Factor,恒定质量因子
GOP Group of Pictures,图像组
I/P/B 帧 帧内/前向预测/双向预测帧
SSIM Structural Similarity,结构相似度
Preset 编码速度预设
Tune 场景优化选项
NALU Network Abstraction Layer Unit
AnnexB H.264 字节流格式
FLV Flash Video,流媒体封装格式
RTMP Real-Time Messaging Protocol

FAQ 常见问题

Q1:本章的核心难点是什么?

A:编码与推流涉及的核心难点包括: - 理解新概念的内在原理 - 将理论知识转化为实际代码 - 处理边界情况和错误恢复

建议多动手实践,遇到问题及时查阅官方文档。


Q2:学习本章需要哪些前置知识?

A:请参考章节头部的前置知识表格。如果某些基础不牢固,建议先复习相关章节。


Q3:如何验证本章的学习效果?

A:建议完成以下检查: - [ ] 理解所有核心概念 - [ ] 能独立编写本章的示例代码 - [ ] 能解释代码的工作原理 - [ ] 能排查常见问题


Q4:本章代码在实际项目中的应用场景?

A:本章代码是渐进式案例「小直播」的组成部分,所有代码都可以在实际项目中使用。具体应用场景请参考「本章与项目的关系」部分。


Q5:遇到问题时如何调试?

A:调试建议: 1. 先阅读 FAQ 和本章的「常见问题」部分 2. 检查前置知识是否掌握 3. 使用日志和调试工具定位问题 4. 参考示例代码进行对比 5. 在 GitHub Issues 中搜索类似问题 —

本章小结

核心知识点

通过本章学习,你应该掌握: 1. 编码与推流的核心概念和原理 2. 相关的 API 和工具使用 3. 实际项目中的应用方法 4. 常见问题的解决方案

关键技能

技能 掌握程度 实践建议
理解核心概念 ⭐⭐⭐ 必须掌握 能向他人解释原理
编写示例代码 ⭐⭐⭐ 必须掌握 独立编写本章代码
排查常见问题 ⭐⭐⭐ 必须掌握 遇到问题时能自行解决
应用到项目 ⭐⭐ 建议掌握 将本章代码集成到项目中

本章产出

下章预告

Ch13:视频编码进阶

为什么要学下一章?

每章都是渐进式案例「小直播」的有机组成部分,下一章将在本章基础上进一步扩展功能。

学习建议: - 确保本章内容已经掌握 - 提前浏览下一章的目录 - 准备好相关的开发环境