核心概念
在开始写代码之前,你需要理解 FFmpeg 解码流程中的几个核心结构体和概念:
AVFormatContext: 格式上下文,包含了文件或流的格式信息,比如视频流、音频流的索引、时长、比特率等,它是整个 FFmpeg 框架的入口。AVCodecContext: 编解码器上下文,包含了特定流(比如视频流)的编解码器参数,比如宽度、高度、像素格式、帧率等,你需要用它来初始化和解码。AVCodec: 编解码器,一个指向具体编解码器实现的指针,H.264、H.265、MPEG-4等,FFmpeg 会根据文件信息自动找到合适的解码器。AVPacket: 数据包,编码后的数据单元,解码器接收的就是一包一包的AVPacket,一个AVPacket可能包含一帧或多帧的画面数据。AVFrame: 原始帧,解码后的原始数据单元,比如解码后的 YUV 或 RGB 图像数据,我们的目标就是得到AVFrame。SwsContext: 软件缩放上下文,FFmpeg 提供的一个强大的软件库,用于图像转换和缩放,我们用它来将解码出来的 YUV 图像(通常是AV_PIX_FMT_YUV420P)转换为易于显示的 RGB 图像。
解码流程总结如下:
- 打开输入文件:使用
avformat_open_input()打开视频文件,并获取AVFormatContext。 - 查找流信息:使用
avformat_find_stream_info()从文件中解析出视频流、音频流等信息。 - 找到视频流:遍历
AVFormatContext中的流,找到类型为AVMEDIA_TYPE_VIDEO的流。 - 查找解码器:根据视频流的
codecpar参数,找到对应的AVCodec。 - 创建解码器上下文:使用
avcodec_alloc_context3()为找到的解码器创建AVCodecContext,并用avcodec_parameters_to_context()将流的参数复制到上下文中。 - 打开解码器:使用
avcodec_open2()打开解码器。 - 读取数据包并解码:
- 循环调用
av_read_frame()从AVFormatContext中读取AVPacket。 AVPacket属于视频流,就调用avcodec_send_packet()将其发送给解码器。- 循环调用
avcodec_receive_frame()从解码器中接收解码后的AVFrame,直到没有更多帧为止。
- 循环调用
- 处理帧数据:得到
AVFrame后,可以使用sws_scale()将其转换为 RGB 格式,然后显示或保存。 - 清理资源:关闭所有打开的对象,释放所有分配的内存。
环境准备
在编译代码之前,你必须确保你的系统上安装了 FFmpeg 开发库。
在 Ubuntu/Debian 上安装
sudo apt update sudo apt install build-essential libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
在 macOS 上安装 (使用 Homebrew)
brew install ffmpeg
在 Windows 上安装
- 从 FFmpeg 官网 下载构建好的库,或者使用 vcpkg 进行安装。
- 将
include目录添加到你的编译器的包含路径中。 - 将
lib目录下的.lib文件添加到你的链接器输入中。 - 将
bin目录添加到系统的PATH环境变量中。
完整代码示例
下面是一个完整的 C 程序,它会解码一个视频文件,并将每一帧保存为 .ppm 格式的图片文件,PPM 是一种简单的、未压缩的图像格式,非常适合用来验证解码结果。
#include <stdio.h>
#include <stdlib.h>
// 引入 FFmpeg 的头文件
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
}
#define SAVE_PPM 1 // 定义宏,决定是否保存为PPM文件
// 保存 AVFrame 为 PPM 文件
void save_frame_as_ppm(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// 生成文件名,如 frame_0001.ppm
sprintf(szFilename, "frame_%04d.ppm", iFrame);
pFile = fopen(szFilename, "wb");
if (pFile == NULL) {
return;
}
// 写入 PPM 文件头
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// 将 AVFrame 中的数据写入文件
for (y = 0; y < height; y++) {
fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
}
fclose(pFile);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <input_file>\n", argv[0]);
return -1;
}
const char *filename = argv[1];
AVFormatContext *pFormatCtx = NULL;
int videoStream, i;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *pFrame = NULL;
AVFrame *pFrameRGB = NULL;
AVPacket *packet = NULL;
int frameFinished;
uint8_t *buffer = NULL;
struct SwsContext *sws_ctx = NULL;
// --- 1. 初始化 FFmpeg 库 ---
av_register_all(); // 在较新版本的FFmpeg中已弃用,但为了兼容性保留
avformat_network_init();
// --- 2. 打开输入文件 ---
if (avformat_open_input(&pFormatCtx, filename, NULL, NULL) != 0) {
fprintf(stderr, "Could not open file '%s'\n", filename);
return -1;
}
// --- 3. 获取流信息 ---
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
fprintf(stderr, "Could not find stream information\n");
return -1;
}
// --- 4. 查找视频流 ---
videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
break;
}
}
if (videoStream == -1) {
fprintf(stderr, "Did not find a video stream\n");
return -1;
}
// --- 5. 获取解码器上下文并查找解码器 ---
AVCodecParameters *pCodecParams = pFormatCtx->streams[videoStream]->codecpar;
pCodec = avcodec_find_decoder(pCodecParams->codec_id);
if (pCodec == NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
pCodecCtx = avcodec_alloc_context3(pCodec);
if (!pCodecCtx) {
fprintf(stderr, "Could not allocate codec context\n");
return -1;
}
// 将流的参数复制到解码器上下文
if (avcodec_parameters_to_context(pCodecCtx, pCodecParams) < 0) {
fprintf(stderr, "Could not copy codec context\n");
return -1;
}
// --- 6. 打开解码器 ---
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
return -1;
}
// --- 7. 分配帧和数据包 ---
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();
packet = av_packet_alloc();
if (!pFrame || !pFrameRGB || !packet) {
fprintf(stderr, "Could not allocate frame or packet\n");
return -1;
}
// 用于转换格式的缓冲区
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
// 设置 pFrameRGB 的数据指针
av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer,
AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
// --- 8. 解码循环 ---
int frame_index = 0;
while (av_read_frame(pFormatCtx, packet) >= 0) {
// 如果是视频流的数据包
if (packet->stream_index == videoStream) {
// 发送数据包到解码器
avcodec_send_packet(pCodecCtx, packet);
// 从解码器中接收解码后的帧
while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
frameFinished = 1; // 成功解码一帧
// --- 9. 转换图像格式 ---
// 创建一个缩放/转换上下文
sws_ctx = sws_getContext(
pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, NULL, NULL, NULL);
if (sws_ctx == NULL) {
fprintf(stderr, "Could not initialize the conversion context\n");
return -1;
}
// 执行转换
sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0,
pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
// --- 10. 保存帧 ---
#if SAVE_PPM
save_frame_as_ppm(pFrameRGB, pCodecCtx->width, pCodecCtx->height, frame_index);
printf("Saved frame %d\n", frame_index);
#endif
frame_index++;
}
}
av_packet_unref(packet); // 释放数据包的引用
}
// --- 11. 清理资源 ---
av_free(buffer);
av_frame_free(&pFrameRGB);
av_frame_free(&pFrame);
av_packet_free(&packet);
avcodec_close(pCodecCtx);
avformat_close_input(pFormatCtx);
sws_freeContext(sws_ctx);
printf("Decoding finished. Total frames: %d\n", frame_index);
return 0;
}
如何编译和运行
-
保存代码:将上面的代码保存为
decoder.c。 -
准备视频文件:准备一个视频文件,
input.mp4,并将其放在与decoder.c相同的目录下。 -
编译:打开终端,使用以下命令进行编译。关键在于链接 FFmpeg 的库。
# 对于 Linux/macOS gcc decoder.c -o decoder -lavformat -lavcodec -lavutil -lswscale # -o decoder: 指定输出的可执行文件名为 decoder # -lavformat: 链接 libavformat # -lavcodec: 链接 libavcodec # -lavutil: 链接 libavutil # -lswscale: 链接 libswscale
Windows (MinGW) 用户:
gcc decoder.c -o decoder.exe -I/path/to/ffmpeg/include -L/path/to/ffmpeg/lib -lavformat -lavcodec -lavutil -lswscale
请将
-I和-L的路径替换为你自己 FFmpeg 库的实际路径。 -
运行:
./decoder input.mp4
运行后,你会在当前目录下看到一系列
frame_XXXX.ppm文件,你可以使用任何图像查看器(如 GIMP, Photoshop, 或一些在线 PPM 查看器)来打开它们,查看解码后的视频帧。
进阶与改进
这个示例是基础,实际应用中你可能需要:
- 显示图像:将 RGB 数据显示在窗口上,而不是保存为文件,这需要使用 GUI 库,如 SDL2、GTK 或 Qt,SDL2 是一个非常流行的选择,因为它专门为多媒体设计,与 FFmpeg 配合得很好。
- 处理音频:流程与视频类似,你需要找到
AVMEDIA_TYPE_AUDIO的流,使用对应的音频解码器(如AAC,MP3)进行解码,然后将 PCM 数据播放出来。 - 错误处理:代码中的错误处理比较简单,一个健壮的程序需要对每一个可能失败的 FFmpeg API 调用都进行返回值检查。
- 多线程:FFmpeg 的
avformat_open_input和avformat_find_stream_info可能很耗时,可以使用avformat_open_input的AVFormatContext的interrupt_callback来实现超时和中断,解码过程也可以放在单独的线程中。 - 硬件加速:现代 CPU 解码高分辨率视频(如 4K H.265)会很吃力,FFmpeg 支持使用 NVIDIA 的 NVDEC、AMD 的 AMF 或 Intel 的 Quick Sync Video 等硬件进行解码,这需要更复杂的初始化过程,但能极大地提升性能。
