FFmpeg音视频开发实战:从采集到播放的完整链路与API详解
音视频开发这个方向,很多人第一次接触 FFmpeg 的时候都会被它庞大的 API 体系劝退。我当初也是,打开官方文档一看,光是结构体就有几十个,什么
AVFormatContext
、
AVCodecContext
、
AVFrame
、
AVPacket
,名字长得像绕口令,完全不知道从哪下手。但真正把采集、编码、封装、解封装、解码、播放这条链路跑通一遍之后,你会发现 FFmpeg 的设计其实非常工整,每个模块各司其职,数据在它们之间像流水线一样传递。这篇内容就是把我自己从零搭这套 demo 的过程完整记录下来,包括每一步为什么这么写、参数怎么定、哪些地方容易翻车。适合有一定 C/C++ 基础、想入门音视频开发但不知道从哪开始的朋友,也适合已经能跑通部分流程、但对整体架构还比较模糊的开发者。
1. 先把整条音视频流水线的骨架搞清楚
1.1 从“采集”到“播放”到底经历了什么
很多人学 FFmpeg 的方式是打开一个 demo 直接抄,抄完能跑就不管了。但一旦换个场景,比如从文件读取换成摄像头采集,或者从 H.264 换成 H.265,就完全不知道怎么改。根本原因是没有建立起整条流水线的全局认知。
我用一个生活化的类比来解释:把音视频处理想象成一条快递物流线。 采集 就是从商家那里取货,拿到的是原始数据(摄像头出来的是 YUV 像素、麦克风出来的是 PCM 采样)。 编码 是把货物压缩打包,因为原始数据太大了,不压缩根本存不下也传不动。 封装 是把打包好的货物装进一个标准集装箱(比如 MP4、MKV),方便运输和存储。 解封装 是到了目的地把集装箱拆开,取出里面的货物包。 解码 是把压缩包还原成原始货物。 播放 就是把还原后的货物展示出来。
这条链路里,FFmpeg 提供的核心能力覆盖了编码、封装、解封装、解码这四个环节。采集和播放严格来说不完全属于 FFmpeg 的职责范围,但 FFmpeg 提供了
libavdevice
来对接各种采集设备,播放则通常需要配合 SDL2 或者 OpenGL 来做渲染。
理解了这个骨架之后,你就知道每个 API 调用处于流水线的哪个位置,出了问题该往哪个环节排查。
1.2 FFmpeg 核心模块的职责划分
FFmpeg 的库分为几个核心模块,每个模块负责流水线上的不同环节:
| 模块 | 职责 | 典型 API |
|---|---|---|
| libavformat | 封装与解封装,处理容器格式 |
avformat_open_input
、
avformat_alloc_output_context2
|
| libavcodec | 编码与解码,处理压缩格式 |
avcodec_send_frame
、avcodec_receive_packet`
|
| libavdevice | 采集设备对接 |
avdevice_register_all
、
av_find_input_format
|
| libswscale | 图像像素格式转换与缩放 |
sws_getContext
、
sws_scale
|
| libswresample | 音频重采样与格式转换 |
swr_alloc_set_opts
、
swr_convert
|
| libavutil | 公共工具,数据结构与数学运算 |
av_frame_alloc
、
av_malloc
|
这个划分非常清晰:
avformat
管容器,
avcodec
管压缩,
avdevice
管设备,
swscale
和
swresample
管格式转换。你在写代码的时候,遇到任何功能需求,先想它属于哪个模块,然后去对应的头文件里找 API,效率会高很多。
注意:FFmpeg 4.0 之后,编码和解码的 API 从旧的
avcodec_encode_video2/avcodec_decode_video2切换到了新的 send/receive 模式。网上很多老教程还在用旧 API,编译会报 deprecated 警告甚至直接报错。建议一律使用新 API。
1.3 新旧 API 的切换为什么让人头疼
FFmpeg 在 4.0 版本做了一次比较大的 API 重构,把编码和解码统一成了 send/receive 模式。这个改动的好处是支持了一次发送、多次接收的异步模式,对硬件编码器和多线程更友好。但代价是网上大量教程和开源项目还在用旧 API,初学者搜到的资料可能是五六年前的,照着写编译不过。
旧 API 的典型写法是:
// 旧 API(已废弃)
avcodec_encode_video2(codec_ctx, &pkt, frame, &got_packet);
avcodec_decode_video2(codec_ctx, frame, &got_frame, &pkt);
新 API 的写法是:
// 新 API
avcodec_send_frame(codec_ctx, frame);
avcodec_receive_packet(codec_ctx, pkt);
avcodec_send_packet(codec_ctx, pkt);
avcodec_receive_frame(codec_ctx, frame);
新 API 的核心逻辑是:你往编码器/解码器里“喂”数据,然后从另一端“取”数据。喂和取是分开的两个函数调用,中间可能返回
AVERROR(EAGAIN)
表示当前喂不进去或者取不出来,需要换另一端操作。这个模式一开始不太习惯,但用多了会发现它比旧 API 更灵活。
2. 环境搭建与编译:别在第一步就卡住
2.1 各平台安装 FFmpeg 开发库的正确姿势
FFmpeg 的安装分两种:一种是装命令行工具(
ffmpeg.exe
、
ffprobe.exe
),另一种是装开发库(头文件 + 链接库)。做开发需要的是后者。
Windows 平台
:推荐从官方推荐的 Windows 构建源下载
shared
和
dev
两个压缩包。
shared
包里是 DLL 和可执行文件,
dev
包里是头文件和
.lib
导入库。解压后把
include
目录配到编译器的头文件搜索路径,
lib
目录配到库搜索路径,
bin
目录加到系统 PATH 里。注意要下载和你的编译器架构匹配的版本,MSVC 用
win64
的,MinGW 也要选对应的。
Linux 平台
:直接用包管理器最省事。Ubuntu/Debian 下
apt install libavformat-dev libavcodec-dev libavdevice-dev libswscale-dev libswresample-dev libavutil-dev
。但要注意发行版自带的 FFmpeg 版本可能比较老,如果你需要新特性(比如某些新的编码器),就得自己编译。自己编译的话,先装
nasm
、
yasm
这些汇编器,然后
./configure --enable-shared --disable-static --enable-gpl --enable-libx264
这样配置。
macOS 平台
:
brew install ffmpeg
会同时装命令行工具和开发库。头文件在
/opt/homebrew/include
(Apple Silicon)或
/usr/local/include
(Intel),库文件在对应的
lib
目录。
提示:如果你在 Windows 上用 MSVC 开发,链接的时候需要链接
avformat.lib、avcodec.lib、avutil.lib、swscale.lib、swresample.lib、avdevice.lib这几个库。顺序有讲究,被依赖的库要放在后面,否则会出现符号找不到的链接错误。
2.2 编译时最常见的三个链接错误及修复
第一个坑是
LNK2019 无法解析的外部符号
。这个错误 90% 的情况是库没链接全,或者链接顺序不对。FFmpeg 的库之间有依赖关系,
avformat
依赖
avcodec
,
avcodec
依赖
avutil
,链接器从左到右解析,所以顺序应该是
avdevice avformat avcodec swscale swresample avutil
。如果顺序反了,链接器在处理
avformat
的时候还没看到
avcodec
的符号,就会报错。
第二个坑是
C 和 C++ 混编时的名称修饰问题
。FFmpeg 是纯 C 库,头文件里没有
extern "C"
包裹。如果你在
.cpp
文件里直接
#include <libavformat/avformat.h>
,C++ 编译器会对函数名做名称修饰,链接的时候就找不到对应的 C 符号了。解决办法是用
extern "C"
包裹:
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}
第三个坑是
运行时找不到 DLL
。编译通过了,一运行就弹窗说缺
avcodec-xx.dll
。这是因为 DLL 不在可执行文件的搜索路径里。解决办法是把 FFmpeg 的
bin
目录加到系统 PATH,或者把需要的 DLL 拷贝到 exe 同目录下。用 Visual Studio 调试的时候,也可以在项目属性里配置调试工作目录。
2.3 验证环境是否就绪的最小测试代码
环境搭好之后,别急着写完整 demo,先用一段最小代码验证一下:
#include <stdio.h>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
int main() {
printf("FFmpeg version: %s\n", av_version_info());
avformat_network_init();
printf("avformat initialized OK\n");
return 0;
}
这段代码只做两件事:打印 FFmpeg 版本号,初始化网络模块。如果能正常编译运行并输出版本号,说明头文件路径、库链接、运行时 DLL 都没问题。如果这一步就报错,先别往下走,把环境问题解决干净。
3. 解封装与解码:从文件到原始帧
3.1 avformat_open_input 背后做了什么
解封装是整条链路的入口。你拿到一个 MP4 文件,第一步是打开它,读取容器信息。核心函数是
avformat_open_input
:
AVFormatContext *fmt_ctx = NULL;
int ret = avformat_open_input(&fmt_ctx, "test.mp4", NULL, NULL);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, sizeof(errbuf));
fprintf(stderr, "Could not open file: %s\n", errbuf);
return -1;
}
这个函数做的事情比看起来多得多。它会探测文件格式(通过扩展名和文件头魔数)、读取容器头部信息、初始化
AVFormatContext
里的各个字段。
AVFormatContext
是整个解封装过程的核心结构体,里面包含了流的数量、每个流的编解码参数、时长、码率等元信息。
打开之后,必须调用
avformat_find_stream_info
来获取更详细的流信息:
ret = avformat_find_stream_info(fmt_ctx, NULL);
if (ret < 0) {
fprintf(stderr, "Failed to get stream info\n");
return -1;
}
这一步会实际读取一部分音视频数据来探测编码参数,比如 H.264 的 SPS/PPS、AAC 的 AudioSpecificConfig。有些格式(比如 MPEG-TS)如果不调用这个函数,流信息是不完整的。
注意:
avformat_open_input和avformat_find_stream_info都可能阻塞,特别是在网络流场景下。如果要做实时性要求高的应用,需要设置AVFormatContext的interrupt_callback来做超时控制。
3.2 找到正确的解码器并打开
拿到流信息之后,需要为每个流找到对应的解码器。以视频流为例:
int video_stream_idx = -1;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
AVCodecParameters *codecpar = fmt_ctx->streams[video_stream_idx]->codecpar;
const AVCodec *codec = avcodec_find_decoder(codecpar->codec_id);
if (!codec) {
fprintf(stderr, "Unsupported codec\n");
return -1;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_ctx, codecpar);
ret = avcodec_open2(codec_ctx, codec, NULL);
这里有几个关键点。第一,FFmpeg 4.0 之后推荐用
AVCodecParameters
而不是直接访问
AVStream->codec
,后者已经废弃了。第二,
avcodec_alloc_context3
创建的是解码器上下文,需要用
avcodec_parameters_to_context
把流里的编码参数拷贝进去。第三,
avcodec_open2
的第三个参数可以传解码器选项,比如
threads
设置多线程解码。
多线程解码是一个很实用的优化。设置
codec_ctx->thread_count = 0
让 FFmpeg 自动根据 CPU 核心数决定线程数,或者手动指定。对于 H.264/H.265 这种帧间压缩的编码,多线程解码能带来接近线性的性能提升。
3.3 解码循环的 send/receive 模式详解
解码循环是新 API 的核心。基本结构是这样的:
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream_idx) {
ret = avcodec_send_packet(codec_ctx, pkt);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
fprintf(stderr, "Error sending packet\n");
break;
}
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
fprintf(stderr, "Error receiving frame\n");
break;
}
// 处理解码后的 frame
process_frame(frame);
av_frame_unref(frame);
}
}
av_packet_unref(pkt);
}
这个循环的逻辑是:从文件读一个 packet,如果属于视频流就送给解码器,然后不断从解码器取 frame 直到取不出来为止。
AVERROR(EAGAIN)
表示当前解码器需要更多输入才能产出输出,或者输出已经取完了需要新的输入。
AVERROR_EOF
表示解码器已经冲刷完毕。
循环结束后,还需要发送一个 NULL packet 来冲刷解码器里缓存的帧:
avcodec_send_packet(codec_ctx, NULL);
while (avcodec_receive_frame(codec_ctx, frame) >= 0) {
process_frame(frame);
av_frame_unref(frame);
}
这一步很容易被忽略。因为解码器内部有缓冲,特别是 B 帧需要等待后续帧才能输出,不冲刷的话最后几帧会丢失。
3.4 解码后的帧格式转换为什么绕不开
解码出来的
AVFrame
通常是 YUV420P 格式,而显示设备需要的是 RGB 格式。这就需要用
libswscale
做转换:
SwsContext *sws_ctx = sws_getContext(
codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
codec_ctx->width, codec_ctx->height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, NULL, NULL, NULL);
AVFrame *rgb_frame = av_frame_alloc();
rgb_frame->format = AV_PIX_FMT_RGB24;
rgb_frame->width = codec_ctx->width;
rgb_frame->height = codec_ctx->height;
av_frame_get_buffer(rgb_frame, 0);
sws_scale(sws_ctx, frame->data, frame->linesize, 0,
codec_ctx->height, rgb_frame->data, rgb_frame->linesize);
sws_getContext
的参数比较多,前三个是输入的分辨率和像素格式,中间三个是输出的分辨率和像素格式,
SWS_BILINEAR
是缩放算法。如果输入输出分辨率一样,缩放算法其实不影响结果,但还是要传一个。
sws_scale
的
linesize
参数很关键。YUV 和 RGB 的每行字节数不一定等于宽度乘以字节数,因为可能有对齐填充。必须用
frame->linesize
而不是自己算,否则图像会出现斜纹或者错位。
提示:
sws_getContext有一定的初始化开销,不要在每帧循环里反复创建和销毁。应该在循环外创建一次,循环结束后再sws_freeContext。
4. 编码与封装:从原始帧到文件
4.1 编码器的选择与参数配置
编码是把原始帧压缩成 packet 的过程。以 H.264 为例:
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
codec_ctx->bit_rate = 400000;
codec_ctx->width = 640;
codec_ctx->height = 480;
codec_ctx->time_base = (AVRational){1, 25};
codec_ctx->framerate = (AVRational){25, 1};
codec_ctx->gop_size = 10;
codec_ctx->max_b_frames = 1;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
avcodec_open2(codec_ctx, codec, NULL);
这几个参数每一个都有讲究。
bit_rate
是目标码率,直接影响画质和文件大小。
time_base
是时间基准,表示每个时间单位是多少秒,这里
{1, 25}
表示 1/25 秒,配合
framerate
为 25 就是每秒 25 帧。
gop_size
是关键帧间隔,10 表示每 10 帧一个 I 帧。
max_b_frames
是 B 帧数量,B 帧能提高压缩率但会增加编码延迟。
pix_fmt
必须和输入帧的格式匹配。大部分编码器只支持 YUV420P,如果你从摄像头采集到的是 YUYV 或者 MJPEG,需要先用
sws_scale
转成 YUV420P。
4.2 编码循环与时间戳的正确设置
编码循环和解码类似,也是 send/receive 模式:
AVFrame *frame = av_frame_alloc();
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
av_frame_get_buffer(frame, 0);
AVPacket *pkt = av_packet_alloc();
for (int i = 0; i < 100; i++) {
av_frame_make_writable(frame);
// 填充 frame 数据...
frame->pts = i;
ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0) break;
while (ret >= 0) {
ret = avcodec_receive_packet(codec_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) break;
pkt->stream_index = video_stream_idx;
av_interleaved_write_frame(fmt_ctx, pkt);
av_packet_unref(pkt);
}
}
frame->pts
是显示时间戳,单位是
codec_ctx->time_base
。这里简单地从 0 递增,实际应用中需要根据采集时间来计算。PTS 设置错误会导致播放速度异常或者音视频不同步。
av_interleaved_write_frame
会自动处理音视频交错写入,保证输出的文件里音视频 packet 按时间顺序排列。如果只有一个流,也可以用
av_write_frame
,但推荐统一用
av_interleaved_write_frame
。
4.3 封装格式的选择与头部写入
封装是把编码后的 packet 写入容器文件。以 MP4 为例:
AVFormatContext *out_fmt_ctx = NULL;
avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, "output.mp4");
AVStream *out_stream = avformat_new_stream(out_fmt_ctx, NULL);
avcodec_parameters_from_context(out_stream->codecpar, codec_ctx);
out_stream->time_base = codec_ctx->time_base;
if (!(out_fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
avio_open(&out_fmt_ctx->pb, "output.mp4", AVIO_FLAG_WRITE);
}
avformat_write_header(out_fmt_ctx, NULL);
avformat_alloc_output_context2
的第三个参数传 NULL 让 FFmpeg 根据文件名自动推断格式,也可以显式指定
"mp4"
或
"matroska"
。
avformat_new_stream
创建一个新的输出流,
avcodec_parameters_from_context
把编码器的参数拷贝到流的
codecpar
里。
avformat_write_header
会写入容器头部。对于 MP4 格式,默认的
moov
box 在文件末尾,这意味着如果程序崩溃,文件可能无法播放。可以通过设置
AVFMT_FLAG_FASTSEEK
或者使用
movflags
选项把
moov
放到前面。
注意:MP4 的
time_base通常是 1/90000 或者 1/1000,而编码器的time_base可能是 1/25。写入 packet 之前需要做时间基转换:av_packet_rescale_ts(pkt, codec_ctx->time_base, out_stream->time_base)。不做转换的话,播放器会按错误的时间戳播放,导致快放或慢放。
4.4 音视频同步封装时的交错策略
当同时封装音频和视频时,交错策略就很重要了。
av_interleaved_write_frame
内部维护了一个 packet 队列,会根据每个流的 PTS 来决定写入顺序。但如果两个流的 PTS 差距太大,队列会不断增长,占用大量内存。
实际项目中,我通常会在编码循环里控制音视频帧的采集节奏,让它们的 PTS 保持接近。比如视频 25fps,音频 44100Hz 每帧 1024 个采样,那么音频帧的 PTS 增量是 1024/44100 秒,视频帧的 PTS 增量是 1/25 秒。两个流各自按自己的节奏走,
av_interleaved_write_frame
会自动处理交错。
如果发现输出文件音视频不同步,优先检查两个流的
time_base
是否设置正确,以及 packet 的 PTS 是否做了正确的 rescale。
5. 采集与播放:打通最后一公里
5.1 用 libavdevice 对接摄像头和麦克风
libavdevice
提供了统一的设备采集接口。在 Windows 上用
dshow
,Linux 上用
v4l2
或
alsa
,macOS 上用
avfoundation
。
以 Windows 的 dshow 为例:
avdevice_register_all();
const AVInputFormat *ifmt = av_find_input_format("dshow");
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "video=Integrated Camera", ifmt, NULL);
设备名称需要先用
ffmpeg -list_devices true -f dshow -i dummy
命令列出来。打开之后,采集流程和解码流程类似,
av_read_frame
读到的就是原始帧或者已编码的帧(取决于设备输出格式)。
摄像头的输出格式通常是 MJPEG 或者 YUYV。MJPEG 的话读到的 packet 需要解码,YUYV 的话需要先转成 YUV420P 再编码。可以在打开设备时通过
AVDictionary
指定格式:
AVDictionary *options = NULL;
av_dict_set(&options, "video_size", "640x480", 0);
av_dict_set(&options, "framerate", "25", 0);
av_dict_set(&options, "pixel_format", "yuyv422", 0);
avformat_open_input(&fmt_ctx, "video=Integrated Camera", ifmt, &options);
5.2 用 SDL2 做视频渲染
FFmpeg 本身不提供显示功能,需要配合 SDL2、OpenGL 或者平台原生 API。SDL2 是最简单的选择:
SDL_Init(SDL_INIT_VIDEO);
SDL_Window *win = SDL_CreateWindow("Player", SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_SHOWN);
SDL_Renderer *renderer = SDL_CreateRenderer(win, -1, 0);
SDL_Texture *texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, width, height);
// 在解码循环里
SDL_UpdateYUVTexture(texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
SDL2 的
SDL_PIXELFORMAT_YV12
对应 FFmpeg 的
AV_PIX_FMT_YUV420P
,可以直接把
AVFrame
的 Y、U、V 三个平面传给
SDL_UpdateYUVTexture
,省去了
sws_scale
转换的开销。这是效率最高的做法。
如果窗口大小和视频分辨率不一致,SDL 会自动缩放,但缩放质量一般。对画质有要求的话,还是用
sws_scale
先缩放到目标分辨率再渲染。
5.3 音频播放的缓冲与同步处理
音频播放比视频复杂,因为音频对连续性要求很高,一旦缓冲区空了就会出现爆音。SDL2 的音频播放需要注册回调函数:
SDL_AudioSpec wanted_spec, obtained_spec;
wanted_spec.freq = 44100;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = 2;
wanted_spec.samples = 1024;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = &audio_buffer;
SDL_OpenAudio(&wanted_spec, &obtained_spec);
SDL_PauseAudio(0);
回调函数里从缓冲区取数据填充到
stream
里。缓冲区需要自己维护,通常用一个环形队列。解码线程往队列里写,音频回调从队列里读。队列空了就填静音,队列满了就丢帧或者等待。
音视频同步通常以音频时钟为基准,因为音频对断续更敏感。视频帧根据音频时钟来调整显示时间,早了就等,晚了就丢。这个逻辑在
SDL_AddTimer
或者主循环里实现。
提示:SDL2 的音频回调运行在独立的音频线程里,不能在里面做耗时操作,也不能调用 SDL 的渲染函数。回调里只做数据拷贝,其他逻辑放到主线程。
6. 踩坑记录与性能调优
6.1 内存泄漏的重灾区与排查方法
FFmpeg 的内存管理是手动式的,每个
av_xxx_alloc
都需要对应的
av_xxx_free
。常见的泄漏点包括:
-
avformat_open_input之后忘记avformat_close_input -
avcodec_alloc_context3之后忘记avcodec_free_context -
av_frame_alloc之后忘记av_frame_free -
av_packet_alloc之后忘记av_packet_free -
sws_getContext之后忘记sws_freeContext -
av_frame_get_buffer分配的缓冲区在av_frame_free时会自动释放,但如果手动av_malloc的就需要手动av_free
排查内存泄漏最有效的工具是 Valgrind(Linux)或者 Visual Studio 的内存诊断工具(Windows)。在 Linux 下:
valgrind --leak-check=full --show-leak-kinds=all ./my_player test.mp4
Valgrind 会列出所有未释放的内存块和分配位置。FFmpeg 内部有一些全局缓存不会释放,这些可以忽略,但你自己代码里分配的一定要清理干净。
6.2 解码花屏和绿屏的常见原因
花屏和绿屏是解码环节最常见的问题,原因通常有这几个:
第一,
packet 不完整
。网络流或者文件损坏导致 packet 数据缺失,解码器拿到残缺数据就会输出花屏。解决办法是在送解码器之前检查 packet 的
size
和
data
是否有效。
第二, 解码器没有正确冲刷 。前面提到的最后几帧丢失问题,如果不发送 NULL packet 冲刷,B 帧相关的帧可能显示异常。
第三,
像素格式不匹配
。解码器输出的格式和渲染器期望的格式不一致,比如解码器输出 NV12 但渲染器按 YUV420P 处理,就会绿屏。解决办法是在
sws_getContext
里明确指定输入格式为
codec_ctx->pix_fmt
。
第四,
linesize 对齐问题
。前面提到过,必须用
frame->linesize
而不是自己算宽度。特别是当宽度不是 16 或 32 的倍数时,对齐填充会导致图像错位。
6.3 编码速度与画质的平衡参数
编码参数直接影响输出文件的大小和画质。以下是我在实际项目中总结的参数调整经验:
| 参数 | 作用 | 调优建议 |
|---|---|---|
| bit_rate | 目标码率 | 720p 建议 2-4 Mbps,1080p 建议 4-8 Mbps |
| gop_size | 关键帧间隔 | 直播场景建议 1-2 秒一个关键帧,点播可以更大 |
| max_b_frames | B 帧数量 | 0 延迟最低,2-3 压缩率较好,再大收益递减 |
| preset | 编码速度预设 | ultrafast 最快但压缩率低,medium 平衡,slow 最慢但压缩率高 |
| crf | 恒定质量因子 | 18-28 之间,数值越小画质越好文件越大 |
preset
和
crf
是 x264/x265 特有的选项,通过
AVDictionary
传给
avcodec_open2
:
AVDictionary *opts = NULL;
av_dict_set(&opts, "preset", "medium", 0);
av_dict_set(&opts, "crf", "23", 0);
avcodec_open2(codec_ctx, codec, &opts);
用
crf
的时候
bit_rate
可以设为 0,让编码器自己决定码率。
crf
模式适合点播场景,直播场景还是用固定码率更可控。
6.4 多线程与硬件加速的取舍
FFmpeg 支持多线程软件编码和硬件加速编码。多线程软件编码通过
codec_ctx->thread_count
设置,对 H.264 编码能带来 2-4 倍的性能提升。硬件加速则依赖具体平台:NVIDIA 的 NVENC、Intel 的 QSV、AMD 的 AMF、Apple 的 VideoToolbox。
硬件编码的优点是速度快、CPU 占用低,缺点是画质通常不如软件编码,而且不同平台的 API 不通用。如果项目需要跨平台,建议软件编码为主,硬件编码作为可选加速。
硬件解码相对更成熟,
avcodec_find_decoder_by_name("h264_cuvid")
可以找到 NVIDIA 的硬件解码器。但硬件解码后的帧在显存里,需要额外的步骤拷贝到内存,增加了复杂度。除非是 4K 高帧率场景,否则软件解码通常够用。
提示:使用硬件加速之前,先用
ffmpeg -hwaccels命令查看当前 FFmpeg 构建支持哪些硬件加速方式。不是所有构建都开启了全部硬件加速选项。
7. 从 demo 到可用工具的差距在哪里
跑通 demo 只是第一步,真正做成一个可用的工具还需要处理很多边界情况。比如网络流的断线重连、采集设备的拔插检测、不同分辨率的自适应、音视频不同步的动态校正、异常数据的容错处理。这些在 demo 里通常不会涉及,但实际项目中一定会遇到。
我的建议是先把 demo 跑通,理解每个 API 的输入输出和调用时机,然后逐步加入错误处理和边界检查。每加一个功能,就用实际数据测试一遍,确认没有引入新的问题。音视频开发没有捷径,就是靠一个个坑踩出来的。
更多推荐
所有评论(0)