在C++工程中处理音视频文件,FFmpeg是最成熟的开源方案之一。它是一套基于C语言编写的库集合,包含封装格式处理、编解码、滤镜等功能模块。通过在其源码基础上编译出动态库,C++程序可以以普通函数调用的方式驱动音视频流水线,而不必每次都启动外部进程。

一、开发环境准备
要在C++中调用FFmpeg,第一步是获取编译好的库文件。Windows平台可前往FFmpeg官网下载shared与dev版本,dev包含头文件和.lib引导库,shared包含运行时需要的.dll。Linux用户通常直接用包管理器安装libavformat-dev、libavcodec-dev等开发包即可。无论哪种系统,核心都是让编译器找得到头文件,链接器找得到库。
以Windows下MinGW或MSVC为例,需把include目录加入附加包含路径,把lib目录加入附加库目录,并在链接输入中增加avformat、avcodec、avutil、swscale等。很多新手编译时报“未定义的引用”,就是因为只包含了头文件却忘了挂库。另外,运行时若提示缺少某某dll,请把shared bin目录里的对应文件拷到exe同层目录或加入PATH。
1.1 基础头文件引用
FFmpeg全部是C接口,在C++里包含时建议用extern "C"包裹,避免名称修饰导致链接失败。下面是最基本的引入方式:
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>
}
上述代码中的引号使用中文弯引号仅为示意,实际编写请使用英文双引号。引入完成后,即可调用诸如avformat_open_input这类函数。注意FFmpeg版本差异较大,旧版需要av_register_all,新版(4.0+)已废弃该调用,直接初始化即可。
二、打开文件并读取流信息
音视频处理的第一步是解封装。FFmpeg用AVFormatContext描述封装格式上下文,通过avformat_open_input打开文件,再用avformat_find_stream_info探测流细节。一个文件可能同时有音频流、视频流、字幕流,每路流在AVFormatContext的streams数组中体现。
下面示例展示如何打开一个本地文件并打印流数量与类型。这段代码没有做错误处理的精简版,实际项目务必检查每个返回值:
extern "C" {
#include <libavformat/avformat.h>
}
#include <iostream>
int main() {
avformat_network_init();
AVFormatContext* fmt_ctx = nullptr;
const char* file = "test.mp4";
if (avformat_open_input(&fmt_ctx, file, nullptr, nullptr) != 0) {
std::cerr << "打开文件失败" << std::endl;
return -1;
}
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
std::cerr << "获取流信息失败" << std::endl;
avformat_close_input(&fmt_ctx);
return -1;
}
std::cout << "流数量: " << fmt_ctx->nb_streams << std::endl;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* s = fmt_ctx->streams[i];
if (s->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
std::cout << "视频流 " << i << std::endl;
} else if (s->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
std::cout << "音频流 " << i << std::endl;
}
}
avformat_close_input(&fmt_ctx);
return 0;
}
运行后你应能看见文件里包含的几路流。若文件本身损坏或路径不对,avformat_open_input会返回负错误码。FFmpeg的错误处理风格统一用返回值小于零表示失败,可用av_strerror转成可读文本。
读取流信息之后,程序就掌握了编码参数,比如视频宽高、帧率、音频采样率等。这些信息位于codecpar中,是后续打开解码器的依据。切记不要直接操作旧版的CodecContext去读参数,新接口一律走AVCodecParameters。
三、解码音视频帧
拿到流索引后,需要找到对应的解码器并创建解码上下文。调用avcodec_find_decoder根据codec_id取得AVCodec,再用avcodec_alloc_context3和avcodec_parameters_to_context把参数填进去,最后avcodec_open2激活。
解码循环通常是:av_read_frame取一个AVPacket,判断所属流,送进avcodec_send_packet,然后反复avcodec_receive_frame拿AVFrame。直到包耗尽、文件末尾。下面的片段演示视频解码取第一帧并输出尺寸:
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
}
#include <iostream>
void decode_video(const char* file) {
AVFormatContext* fmt = nullptr;
avformat_open_input(&fmt, file, nullptr, nullptr);
avformat_find_stream_info(fmt, nullptr);
int v_idx = -1;
for (unsigned int i = 0; i < fmt->nb_streams; i++) {
if (fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
v_idx = i; break;
}
}
AVCodecParameters* par = fmt->streams[v_idx]->codecpar;
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);
AVPacket* pkt = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
while (av_read_frame(fmt, pkt) >= 0) {
if (pkt->stream_index == v_idx) {
avcodec_send_packet(ctx, pkt);
if (avcodec_receive_frame(ctx, frame) == 0) {
std::cout << "解码一帧 宽:" << frame->width
<< " 高:" << frame->height << std::endl;
break;
}
}
av_packet_unref(pkt);
}
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&ctx);
avformat_close_input(&fmt);
}
这段代码只取了首帧做演示。真实场景里要循环接收直到函数返回AVERROR(EAGAIN)或EOF,并且每次send之后可能对应多个receive。AVPacket和AVFrame都用完后必须调用对应的free与unref,否则会出现内存泄漏。
音频解码流程完全一致,只是codec_type换成AVMEDIA_TYPE_AUDIO,拿到的frame里是采样数据而非图像。若要做音视频同步播放,还需依据frame的pts做时钟对齐,这超出了基础调用范畴,但底层API就是上述这一套。
四、重新封装与转码思路
如果只是读取而不写出,那叫解封装分析;若要处理完再存成新文件,需要用到avformat_alloc_output_context2创建输出上下文,通过avio_open打开输出,然后写头部、写帧、写尾部。转码则介于两者之间:先解码出AVFrame,再用目标编码器avcodec_send_frame编码成AVPacket,最后写入输出格式。
一个常见误区是以为C++调用FFmpeg必须写一大堆滤镜链。其实基础转封装(如mp4转flv)可以不解码,直接把读到的packet改下时间戳写入新格式,速度极快且零画质损失。只有改分辨率、加水印才需要走swscale或滤镜图。明确需求能大幅简化代码。
| 操作类型 | 是否解码 | 典型API |
|---|---|---|
| 格式转换 | 否 | av_read_frame + av_interleaved_write_frame |
| 分辨率缩放 | 是 | sws_scale |
| 码率重编码 | 是 | avcodec_send_frame / receive_packet |
上表列出了三种常见任务的区别。新人往往一上来就全解码重编码,既慢又容易引入质量误差。理解封装层与编解码层的边界,是写好C++音视频程序的关键。
五、编译与链接注意事项
在Linux下用g++编译上述程序,命令大致为:g++ main.cpp -o app $(pkg-config --cflags --libs libavformat libavcodec),pkg-config会自动补上头文件路径和-l库名。Windows下若用CMake,可写find_package或手动target_link_libraries指定avformat、avcodec、avutil。
调试阶段建议打开FFmpeg日志:av_log_set_level(AV_LOG_DEBUG),这样当解码失败时能看见具体原因,比如不支持的像素格式。另外,FFmpeg大量使用指针与手动内存管理,C++侧最好用智能指针或封装类包裹,防止异常路径下忘了释放。只要环境搭好、API调顺,C++控制FFmpeg处理音视频就变得像调用普通动态库一样直观可靠。