一、 环境配置与头文件规范
在 C++ 项目中,必须通过 extern "C" 屏蔽 C++ 的符号修饰,否则编译器无法正确链接 FFmpeg 的 C 函数库。
1. 核心头文件及其职责
C++
extern "C" {
#include <libavformat/avformat.h> // 【解封装】负责打开文件、读取网络流(RTSP/RTMP)
#include <libavcodec/avcodec.h> // 【编解码】负责把 H.264/H.265 数据解压成像素
#include <libswscale/swscale.h> // 【图像转换】负责 YUV 转 RGB,以及画面缩放
#include <libavutil/imgutils.h> // 【图像工具】负责内存分配、计算图像大小
#include <libavutil/log.h> // 【日志系统】负责调试和排错
}
二、 调试排错:日志系统 (必用)
FFmpeg 内部运行极其复杂,不打印日志就相当于盲人摸象。在 main() 函数开头调用即可设置级别。
1. 设置示例
C++
av_log_set_level(AV_LOG_DEBUG); // 开发阶段必选,能看到详细的网络握手和解码细节
2. 日志级别速查表
| 级别 | 用途 | 备注 |
|---|---|---|
| AV_LOG_ERROR | 发生崩溃或无法继续的错误 | 如:解码器打开失败 |
| AV_LOG_WARNING | 异常但不致命 | 如:视频流丢包 |
| AV_LOG_INFO | 打印流信息 | 默认级别,显示宽、高、码率 |
| AV_LOG_DEBUG | 调试首选 | 显示底层交互细节,排查网络流必选 |
三、 内存中的"四大金刚" (核心数据结构)
AI 开发本质上就是在这四个关键结构体之间搬运和转换数据:
-
AVFormatContext:集装箱。包含了视频的所有流信息。
-
AVCodecContext:加工厂。负责具体的编解码逻辑。
-
AVPacket :压缩包。存放 H.264/H.265 数据,AI 看不懂。
-
AVFrame :原始帧。存放 YUV 或 RGB 像素,AI 推理的直接原材料。
-
H.264/H.265:是视频的"压缩包"。
-
解码(Decoding) :就是把这个压缩包(
AVPacket)解压成 AI 认识的像素图片(AVFrame)
四、 实战项目:视频抽帧与格式转换 (C++完整代码)
逻辑流程:
打开视频 -> 找到视频流 ->初始化解码器 ->准备图像格式转换器 (YUV420P -> RGB24)->为 RGB 帧手动分配内存空间->循环读取 AVPacket-> 解码成 AVFrame (YUV) ->转换成 RGB-> AI 处理/保存。
C++
#include <iostream>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}
int main() {
// 设置日志级别
av_log_set_level(AV_LOG_INFO);
const char* url = "test.mp4"; // 支持本地文件或 RTSP 地址
// 1. 打开流并获取信息
AVFormatContext* fmt_ctx = avformat_alloc_context();//1. 申请内存并初始化默认值
if (avformat_open_input(&fmt_ctx, url, NULL, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "无法打开输入流\n");
return -1;
}
avformat_find_stream_info(fmt_ctx, NULL);
// 第二步:判断能不能读懂里面的内容(格式对不对?有没有坏帧?)
// 2. 查找视频流索引与解码器
//如果你打开一个电影,nb_streams 可能是 3(1路视频 + 1路中文音频 + 1路英文字幕)一共三个流。
int v_idx = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
//查看流类型,找到视频流, v_idx存放的是视频流编号
v_idx = i; break;
}
}
AVCodecParameters* c_par = fmt_ctx->streams[v_idx]->codecpar;//拿到编码参数
//里面有什么:这个视频是用什么编码的(H.264?)、宽高是多少、像素格式(YUV420P?)等。
const AVCodec* codec = avcodec_find_decoder(c_par->codec_id);//寻找解码器
AVCodecContext* c_ctx = avcodec_alloc_context3(codec);//创建解码器上下文,为解码
//过程开辟一块内存空间
avcodec_parameters_to_context(c_ctx, c_par);//同步参数,让解码器知道视频的宽、高、码率
avcodec_open2(c_ctx, codec, NULL);//正式打开解码器
// 3. 准备图像格式转换器 (YUV420P -> RGB24)
// AI 推理通常需要 RGB 格式
SwsContext* sws_ctx = sws_getContext(
c_ctx->width, c_ctx->height, c_ctx->pix_fmt, // 原图宽高及格式
c_ctx->width, c_ctx->height, AV_PIX_FMT_RGB24, // 目标宽高及格式
SWS_BILINEAR, NULL, NULL, NULL
);
// 4. 准备存放数据的容器(内存分配)
AVPacket* pkt = av_packet_alloc();
AVFrame* frame = av_frame_alloc(); // 接收解码后的原始 YUV
AVFrame* frame_rgb = av_frame_alloc(); // 接收转换后的目标 RGB
// 为 RGB 帧手动分配内存空间
int num_bytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, c_ctx->width, c_ctx->height, 1);//计算出存放一帧图片到底需要多少字节
uint8_t* buffer = (uint8_t*)av_malloc(num_bytes);
//把刚才申请的 buffer 的地址,"填"到 frame_rgb 结构体的 data 指针数组里。
//从此以后,你往 frame_rgb->data 里写数据,实际上就是写到了 buffer 这块内存里。
av_image_fill_arrays(frame_rgb->data, frame_rgb->linesize, buffer,
AV_PIX_FMT_RGB24, c_ctx->width, c_ctx->height, 1);
//frame_rgb->linesize写完一行后,要跳过多少字节才能开始写下一行。
// 5. 解码主循环
int count = 0;
while (av_read_frame(fmt_ctx, pkt) >= 0 && count < 10) {
if (pkt->stream_index == v_idx) {
// 发送压缩包到解码器
if (avcodec_send_packet(c_ctx, pkt) == 0) {
// 接收解码后的帧
while (avcodec_receive_frame(c_ctx, frame) == 0) {
// 【关键步骤】执行像素格式转换:YUV -> RGB
sws_scale(sws_ctx, frame->data, frame->linesize, 0, c_ctx->height,
frame_rgb->data, frame_rgb->linesize);
//0 (起始行):表示从输入图像的 第 0 行(最顶部)开始处理
//c_ctx->height (扫描行数):表示本次处理 连续的多少行。在这里填入视频的总高度,意思就是"处理完整的一帧"。
// 🎯 此时 frame_rgb->data[0] 存储的就是 RGB 裸数据
// 可以直接传给 OpenCV Mat 或 TensorRT 推理引擎
av_log(NULL, AV_LOG_INFO, "已处理第 %d 帧视频\n", ++count);
}
}
}
av_packet_unref(pkt); // 必须释放 AVPacket,防止内存泄漏
}
// 6. 资源清理
av_free(buffer);
av_frame_free(&frame);
av_frame_free(&frame_rgb);
av_packet_free(&pkt);
avcodec_free_context(&c_ctx);
avformat_close_input(&fmt_ctx);
sws_freeContext(sws_ctx);
return 0;
}
五、 编译与链接命令 (Linux/WSL)
在终端编译时,必须明确指定 FFmpeg 的安装路径并链接相关库。
Bash
g++ main.cpp -o ai_vision_demo \
-I/usr/local/ffmpeg/include \
-L/usr/local/ffmpeg/lib \
-lavformat -lavcodec -lavutil -lswscale \
-Wl,-rpath=/usr/local/ffmpeg/lib
💡 学习建议与 AI 集成笔记
-
目标检测集成 :在
sws_scale之后,frame_rgb->data[0]实际上就是一个一维数组。如果你使用 OpenCV,可以用cv::Mat(height, width, CV_8UC3, frame_rgb->data[0])直接封装。 -
画面缩放 :如果你需要将 1080P 缩放到 640x640 喂给 YOLO,只需修改
sws_getContext里的目标宽高参数即可。 -
RTSP 稳定性 :如果是接入网络摄像头,建议增加
AVDictionary参数设置rtsp_transport为tcp以防止花屏。
cpp
// 1. 定义一个配置字典指针
AVDictionary* options = NULL;
// 2. 设置 RTSP 传输协议为 TCP (默认通常是 UDP)
// 参数解释:"rtsp_transport" 是键,"tcp" 是值
av_dict_set(&options, "rtsp_transport", "tcp", 0);
// 3. (可选) 设置超时时间,防止摄像头掉线导致程序永久卡死
// 单位是微秒,这里设置 5 秒超时
av_dict_set(&options, "stimeout", "5000000", 0);
// 4. 打开输入流,注意第四个参数传入了 &options
// avformat_open_input 内部会读取这些设置
if (avformat_open_input(&fmt_ctx, url, NULL, &options) < 0) {
av_log(NULL, AV_LOG_ERROR, "无法打开网络流\n");
return -1;
}
// 5. 关键:打开后记得释放 dictionary
// 即使打开失败了,也要调用这个来防内存泄漏
av_dict_free(&options);