6、FFmpeg编码实战:FFmpeg编码流程、音频编码、视频编码、编码参数调优

编码这块,说白了就是把原始的音视频数据“压缩”成更小的文件。你想想看,一帧1080p的原始图像,动不动就好几MB,不压缩根本没法存、没法传。FFmpeg就是干这个的利器。我个人习惯把编码流程拆成三步走:打开编码器、喂数据、收数据。听起来简单,但坑不少,咱们一个一个说。

6.1 FFmpeg编码核心流程

不管你是编音频还是视频,FFmpeg的套路都差不多。我总结了一个通用模板,你照着写基本不会错。

编码五步走:

  1. 创建编码器上下文——找到编码器,分配AVCodecContext
  2. 设置编码参数——码率、帧率、采样率这些,一个都不能少
  3. 打开编码器——avcodec_open2(),成败在此一举
  4. 发送帧——avcodec_send_frame(),把原始数据丢进去
  5. 接收包——avcodec_receive_packet(),拿到压缩后的数据

嗯,这里要注意:发送和接收不是一一对应的。有时候你发一帧,可能收不到包;有时候发一帧,能收到好几个包。这是编码器的内部缓冲机制在作怪。我在项目中遇到过,刚上手时以为每次send_frame后必须receive_packet,结果漏了不少数据。

// 核心编码循环示例
AVFrame *frame = /* 从原始数据填充 */;
AVPacket *pkt = av_packet_alloc();

int ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0) {
    // 处理错误
}

while (ret >= 0) {
    ret = avcodec_receive_packet(enc_ctx, pkt);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        break;
    }
    // 这里pkt就是编码后的数据,写入文件或推流
    av_packet_unref(pkt);
}

避坑指南:我曾经在循环里忘了av_packet_unref(),结果内存暴涨。记住,每次用完pkt一定要unref,否则内存泄漏等着你。

6.2 音频编码实战

音频编码相对简单,因为数据量小,参数也少。但有个关键点——采样格式和通道布局。FFmpeg内部对音频数据有严格的格式要求,不是你随便塞个float数组就能用的。

我个人习惯用AV_SAMPLE_FMT_FLTP,也就是浮点型平面模式。为什么?因为大多数编码器(比如AAC)都支持这个格式,而且精度高,不容易出现量化噪声。

// 音频编码器配置示例
AVCodecContext *audio_ctx = avcodec_alloc_context3(codec);
audio_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;
audio_ctx->sample_rate = 44100;
audio_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
audio_ctx->channels = 2;
audio_ctx->bit_rate = 128000; // 128kbps

// 别忘了设置frame_size,很多编码器要求固定帧大小
audio_ctx->frame_size = 1024;

你想想看,为什么frame_size要设成1024?其实这是AAC编码器的老传统了。我记得第一次做音频编码时,设了个2048,结果avcodec_open2直接报错。后来查文档才知道,AAC编码器只支持1024或960这两种帧大小。

注意:音频编码时,输入帧的nb_samples必须等于编码器要求的frame_size。如果你从麦克风采集的数据量不对,需要先做重采样。av_resample或swr_convert都能干这事。

6.3 视频编码实战

视频编码就复杂多了。H.264、H.265、VP9……每种编码器都有自己的脾气。我主要用H.264,兼容性好,而且硬件加速支持广泛。

视频编码的核心参数就几个:分辨率、帧率、码率、GOP大小。但真正影响画质的,其实是码率控制模式。FFmpeg支持三种:

模式 参数设置 适用场景
CBR(固定码率) bit_rate + rc_min_rate/rc_max_rate 相等 直播、视频通话,带宽稳定
VBR(可变码率) bit_rate + rc_min_rate/rc_max_rate 不同 本地录制,画质优先
CRF(恒定质量) crf=18~28,不设bit_rate 离线转码,文件大小不重要

我个人最常用CRF模式。为什么?因为它省心。你设个crf=23,编码器会自动分配码率,画面复杂的地方多给点,简单的地方少给点。我在项目中做过测试,crf=23的H.264视频,肉眼几乎看不出和原始画面的区别,但文件大小能压缩到原来的1/50。

// 视频编码器配置(H.264 CRF模式)
AVCodecContext *video_ctx = avcodec_alloc_context3(codec);
video_ctx->codec_id = AV_CODEC_ID_H264;
video_ctx->width = 1920;
video_ctx->height = 1080;
video_ctx->time_base = (AVRational){1, 30};
video_ctx->framerate = (AVRational){30, 1};
video_ctx->pix_fmt = AV_PIX_FMT_YUV420P;

// CRF模式:不设bit_rate,设crf
av_opt_set_int(video_ctx->priv_data, "crf", 23, 0);
// 预设:medium是平衡点,slow画质更好但编码慢
av_opt_set(video_ctx->priv_data, "preset", "medium", 0);

关键点:视频编码的输入帧必须是YUV420P格式。如果你从摄像头拿到的是RGB,记得用sws_scale转换。我曾经偷懒没转,结果编码出来的视频全是绿色的,排查了半天才发现是色彩空间不对。

6.4 编码参数调优

调优这事,说白了就是平衡三要素:画质、码率、编码速度。你不可能三个都要,必须有所取舍。

我总结了一套调优口诀,分享给你:

  • 画质优先:用CRF模式,crf设18~20,preset用slow或veryslow
  • 码率优先:用VBR模式,设目标码率,允许波动
  • 速度优先:用CBR模式,preset用ultrafast或superfast

嗯,这里有个坑。preset从ultrafast到veryslow,编码速度能差10倍以上,但画质提升其实有限。我在项目中做过对比:medium和slow的编码时间差了3倍,但PSNR只提高了0.3dB。所以除非你对画质有极致要求,否则medium就够用了。

调优小技巧:对于H.264,可以试试开启"trellis"量化。av_opt_set(video_ctx->priv_data, "trellis", "1", 0)。这个参数能在码率不变的情况下提升画质,但编码时间会增加20%左右。值不值得,看你项目需求。

还有一个容易被忽略的参数——GOP大小。GOP就是两个关键帧之间的间隔。设小了,文件变大但随机寻址快;设大了,文件小但拖动进度条会卡。我个人习惯直播场景设GOP=30(1秒1个关键帧),本地录制设GOP=150(5秒1个)。

警告:千万别把GOP设得太大。我曾经图省事设了GOP=600,结果视频文件是变小了,但用户拖动进度条时,要等好几秒才能出画面。用户体验极差。

6.5 编码流程全景图

下面这张图,是我梳理的FFmpeg编码完整流程。从原始数据到编码输出,每一步都标清楚了。你照着这个流程写代码,基本不会漏东西。

FFmpeg编码流程全景图 原始数据 AVFrame (YUV/PCM) 编码器上下文 AVCodecContext 发送帧 avcodec_send_frame() 接收包 avcodec_receive_packet() 编码输出 AVPacket (H.264/AAC) 循环直到AVERROR(EAGAIN) 编码参数调优三要素 🎯 画质 CRF模式,crf=18~20 📊 码率 VBR模式,设目标码率 ⚡ 速度 preset=ultrafast~slow

这张图里,我特意把参数调优放在底部。为什么?因为调优不是编码流程的一部分,而是贯穿始终的思考。你每设一个参数,都要想清楚:这个改动会影响画质还是码率还是速度?

好了,编码实战的核心内容就这些。记住,多动手写代码,多跑几个测试用例,你很快就能摸清FFmpeg的脾气。遇到问题别慌,av_err2str()能帮你把错误码转成可读的字符串,这是调试的第一利器。