FFmpeg 原生 API 压缩图片和视频完全指南
目录
1. FFmpeg 核心概念
1.1 FFmpeg 架构图
┌─────────────────────────────────────────────────────────┐
│ FFmpeg 架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ libavformat (封装格式处理) │ │
│ │ - Demuxer: 解封装 (MP4 → 视频流 + 音频流) │ │
│ │ - Muxer: 封装 (视频流 + 音频流 → MP4) │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ libavcodec (编解码器) │ │
│ │ - Decoder: 解码 (压缩数据 → 原始帧) │ │
│ │ - Encoder: 编码 (原始帧 → 压缩数据) │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ libavutil (工具库) │ │
│ │ - 内存管理 │ │
│ │ - 像素格式转换 │ │
│ │ - 工具函数 │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ libswscale (图像缩放与格式转换) │ │
│ │ - 尺寸调整 │ │
│ │ - 像素格式转换 (YUV ↔ RGB) │ │
│ └──────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ libswresample (音频重采样) │ │
│ │ - 采样率转换 │ │
│ │ - 声道布局转换 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
1.2 核心数据结构
// 1. AVFormatContext - 封装格式上下文
// 代表一个音视频文件,包含所有流信息
AVFormatContext *formatCtx;
- nb_streams: 流的数量
- streams[]: 流数组(视频流、音频流、字幕流)
- duration: 时长
- bit_rate: 比特率
// 2. AVStream - 流
// 代表文件中的一个流(视频、音频或字幕)
AVStream *stream;
- index: 流的索引
- codecpar: 编解码器参数
- time_base: 时间基准
// 3. AVCodecParameters - 编解码器参数
AVCodecParameters *codecPar;
- codec_id: 编解码器 ID (如 AV_CODEC_ID_H264)
- codec_type: 编解码器类型 (视频/音频)
- width/height: 视频尺寸
- format: 像素格式
- bit_rate: 比特率
// 4. AVCodecContext - 编解码器上下文
AVCodecContext *codecCtx;
- 包含编解码器的所有状态信息
- 用于实际的编解码操作
// 5. AVCodec - 编解码器
const AVCodec *codec;
- 编解码器的实现
- 包含编码和解码函数
// 6. AVPacket - 数据包
AVPacket *packet;
- 存储压缩后的数据(编码后的帧)
- 包含时间戳、持续时间等信息
// 7. AVFrame - 帧
AVFrame *frame;
- 存储原始数据(解码后的帧或待编码的帧)
- 包含像素数据、格式、尺寸等
// 8. SwsContext - 缩放与格式转换上下文
struct SwsContext *swsCtx;
- 用于图像缩放和像素格式转换
1.3 FFmpeg 处理流程
┌─────────────────────────────────────────────────────────┐
│ 图片/视频压缩流程 │
└─────────────────────────────────────────────────────────┘
输入文件
↓
┌─────────────────────────────────────────────────────────┐
│ 1. 解封装 (Demux) │
│ avformat_open_input() │
│ avformat_find_stream_info() │
│ av_find_best_stream() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. 查找解码器 │
│ avcodec_find_decoder() │
│ avcodec_alloc_context3() │
│ avcodec_parameters_to_context() │
│ avcodec_open2() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. 解码 (Decode) │
│ av_read_frame() │
│ avcodec_send_packet() │
│ avcodec_receive_frame() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 处理 (Process) │
│ - 图像缩放: sws_scale() │
│ - 格式转换: sws_scale() │
│ - 滤镜: avfilter_graph_alloc() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. 查找编码器 │
│ avcodec_find_encoder_by_name() │
│ avcodec_alloc_context3() │
│ avcodec_open2() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 6. 编码 (Encode) │
│ avcodec_send_frame() │
│ avcodec_receive_packet() │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 7. 封装输出 (Mux) │
│ avformat_alloc_output_context2() │
│ avformat_new_stream() │
│ avio_open() │
│ avformat_write_header() │
│ av_interleaved_write_frame() │
│ av_write_trailer() │
└─────────────────────────────────────────────────────────┘
↓
输出文件
2. 开发环境配置
2.1 引入 FFmpeg 头文件
// 必须在 extern "C" 块中引入,因为 FFmpeg 是 C 语言编写的
extern "C" {
#include "libavformat/avformat.h" // 封装格式
#include "libavcodec/avcodec.h" // 编解码
#include "libavutil/imgutils.h" // 图像工具
#include "libavutil/error.h" // 错误处理
#include "libswscale/swscale.h" // 缩放和格式转换
}
2.2 初始化 FFmpeg (FFmpeg 4.x+ 不需要,但保留兼容性)
// FFmpeg 4.x 之前需要初始化,现在已弃用但仍可调用
// av_register_all(); // 已弃用
// avcodec_register_all(); // 已弃用
// FFmpeg 4.x+ 不需要手动初始化
// 所有编解码器和格式会自动注册
2.3 日志配置
// 设置 FFmpeg 日志级别
// AV_LOG_ERROR - 只输出错误
// AV_LOG_WARNING - 输出警告和错误
// AV_LOG_INFO - 输出信息、警告和错误
// AV_LOG_DEBUG - 输出调试信息
// AV_LOG_TRACE - 输出所有信息(非常详细)
av_log_set_level(AV_LOG_INFO);
// 自定义日志回调
void custom_log_callback(void *ptr, int level, const char *fmt, va_list vl) {
// 自定义日志处理
__android_log_vprint(ANDROID_LOG_INFO, "FFmpeg", fmt, vl);
}
// 设置自定义日志回调
av_log_set_callback(custom_log_callback);
3. 图片压缩实现
3.1 完整的图片压缩函数
/**
* 压缩图片
*
* @param inputPath 输入图片路径
* @param outputPath 输出图片路径
* @param targetWidth 目标宽度(0 表示保持原宽度)
* @param targetHeight 目标高度(0 表示保持原高度)
* @param quality 压缩质量 (1-100, 数值越高质量越好)
* @return 成功返回 0,失败返回负数
*/
int compress_image(const char *inputPath,
const char *outputPath,
int targetWidth,
int targetHeight,
int quality) {
// ==================== 变量声明 ====================
AVFormatContext *inputFormatCtx = NULL; // 输入格式上下文
AVFormatContext *outputFormatCtx = NULL; // 输出格式上下文
AVCodecContext *decoderCtx = NULL; // 解码器上下文
AVCodecContext *encoderCtx = NULL; // 编码器上下文
SwsContext *swsCtx = NULL; // 缩放上下文
AVFrame *inputFrame = NULL; // 输入帧(原始)
AVFrame *outputFrame = NULL; // 输出帧(处理后)
AVPacket *packet = NULL; // 数据包
const AVCodec *decoder = NULL; // 解码器
const AVCodec *encoder = NULL; // 编码器
int videoStreamIndex = -1; // 视频流索引
int ret = 0; // 返回值
// ==================== 步骤 1: 打开输入文件 ====================
// 1.1 打开输入文件
// 参数说明:
// - &inputFormatCtx: 格式上下文指针的地址
// - inputPath: 输入文件路径
// - NULL: 输入格式(NULL 表示自动检测)
// - NULL: 配置选项(通常为 NULL)
ret = avformat_open_input(&inputFormatCtx, inputPath, NULL, NULL);
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
fprintf(stderr, "无法打开输入文件: %s\n", errorBuf);
return ret;
}
// 1.2 读取流信息
// 这个函数会读取文件的一部分来获取流信息
ret = avformat_find_stream_info(inputFormatCtx, NULL);
if (ret < 0) {
fprintf(stderr, "无法获取流信息\n");
goto cleanup;
}
// 1.3 查找视频流
// AVMEDIA_TYPE_VIDEO: 查找视频流
// -1: 不限制相关流
// -1: 不限制解码器
// NULL: 不返回解码器
// 0: 标志位
videoStreamIndex = av_find_best_stream(
inputFormatCtx,
AVMEDIA_TYPE_VIDEO,
-1, -1, NULL, 0
);
if (videoStreamIndex < 0) {
fprintf(stderr, "未找到视频流\n");
ret = videoStreamIndex;
goto cleanup;
}
AVStream *inputStream = inputFormatCtx->streams[videoStreamIndex];
// ==================== 步骤 2: 初始化解码器 ====================
// 2.1 查找解码器
// codec_id 指定了编解码器的 ID
decoder = avcodec_find_decoder(inputStream->codecpar->codec_id);
if (!decoder) {
fprintf(stderr, "未找到解码器\n");
ret = AVERROR(EINVAL);
goto cleanup;
}
// 2.2 分配解码器上下文
decoderCtx = avcodec_alloc_context3(decoder);
if (!decoderCtx) {
fprintf(stderr, "无法分配解码器上下文\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 2.3 将流的参数复制到解码器上下文
ret = avcodec_parameters_to_context(decoderCtx, inputStream->codecpar);
if (ret < 0) {
fprintf(stderr, "无法复制解码器参数\n");
goto cleanup;
}
// 2.4 打开解码器
ret = avcodec_open2(decoderCtx, decoder, NULL);
if (ret < 0) {
fprintf(stderr, "无法打开解码器\n");
goto cleanup;
}
// ==================== 步骤 3: 计算输出尺寸 ====================
int inputWidth = decoderCtx->width;
int inputHeight = decoderCtx->height;
// 如果目标尺寸为 0 或大于原始尺寸,保持原始尺寸
if (targetWidth <= 0 || targetWidth > inputWidth) {
targetWidth = inputWidth;
}
if (targetHeight <= 0 || targetHeight > inputHeight) {
targetHeight = inputHeight;
}
printf("原始尺寸: %dx%d\n", inputWidth, inputHeight);
printf("目标尺寸: %dx%d\n", targetWidth, targetHeight);
// ==================== 步骤 4: 初始化编码器 ====================
// 4.1 查找编码器(使用 MJPEG 编码器,适合图片)
encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
if (!encoder) {
fprintf(stderr, "未找到编码器\n");
ret = AVERROR(EINVAL);
goto cleanup;
}
// 4.2 分配编码器上下文
encoderCtx = avcodec_alloc_context3(encoder);
if (!encoderCtx) {
fprintf(stderr, "无法分配编码器上下文\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 4.3 设置编码参数
encoderCtx->width = targetWidth;
encoderCtx->height = targetHeight;
encoderCtx->pix_fmt = AV_PIX_FMT_YUVJ420P; // JPEG 使用的像素格式
encoderCtx->time_base = (AVRational){1, 25}; // 时间基准
// 设置质量参数
// 注意: 不同编码器的质量参数设置方式不同
// MJPEG 使用 qscale (质量系数)
// 范围: 1-31, 数值越小质量越好
// 转换 quality (1-100) 到 qscale (1-31)
encoderCtx->flags |= AV_CODEC_FLAG_QSCALE; // 启用质量模式
encoderCtx->global_quality = FF_QP2LAMBDA * (31 - quality * 30 / 100);
// 4.4 打开编码器
ret = avcodec_open2(encoderCtx, encoder, NULL);
if (ret < 0) {
fprintf(stderr, "无法打开编码器\n");
goto cleanup;
}
// ==================== 步骤 5: 创建输出文件 ====================
// 5.1 分配输出格式上下文
ret = avformat_alloc_output_context2(
&outputFormatCtx,
NULL, // 输出格式(NULL 表示自动检测)
NULL, // 格式名称
outputPath // 输出文件名
);
if (ret < 0) {
fprintf(stderr, "无法分配输出格式上下文\n");
goto cleanup;
}
// 5.2 创建输出流
AVStream *outputStream = avformat_new_stream(outputFormatCtx, NULL);
if (!outputStream) {
fprintf(stderr, "无法创建输出流\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 5.3 复制编码器参数到输出流
ret = avcodec_parameters_from_context(outputStream->codecpar, encoderCtx);
if (ret < 0) {
fprintf(stderr, "无法复制编码器参数\n");
goto cleanup;
}
// 5.4 打开输出文件
ret = avio_open(&outputFormatCtx->pb, outputPath, AVIO_FLAG_WRITE);
if (ret < 0) {
fprintf(stderr, "无法打开输出文件\n");
goto cleanup;
}
// 5.5 写入文件头
ret = avformat_write_header(outputFormatCtx, NULL);
if (ret < 0) {
fprintf(stderr, "无法写入文件头\n");
goto cleanup;
}
// ==================== 步骤 6: 初始化缩放上下文 ====================
// 如果需要缩放或格式转换
if (inputWidth != targetWidth || inputHeight != targetHeight ||
decoderCtx->pix_fmt != encoderCtx->pix_fmt) {
// 分配缩放上下文
swsCtx = sws_getContext(
inputWidth, inputHeight, decoderCtx->pix_fmt, // 输入
targetWidth, targetHeight, encoderCtx->pix_fmt, // 输出
SWS_BICUBIC, // 缩放算法(双三次插值)
NULL, NULL, NULL
);
if (!swsCtx) {
fprintf(stderr, "无法初始化缩放上下文\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
}
// ==================== 步骤 7: 分配帧和包 ====================
// 7.1 分配输入帧
inputFrame = av_frame_alloc();
if (!inputFrame) {
fprintf(stderr, "无法分配输入帧\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 7.2 分配输出帧
outputFrame = av_frame_alloc();
if (!outputFrame) {
fprintf(stderr, "无法分配输出帧\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 7.3 设置输出帧参数
outputFrame->width = targetWidth;
outputFrame->height = targetHeight;
outputFrame->format = encoderCtx->pix_fmt;
// 7.4 为输出帧分配缓冲区
ret = av_frame_get_buffer(outputFrame, 0); // 0 = 默认对齐
if (ret < 0) {
fprintf(stderr, "无法分配输出帧缓冲区\n");
goto cleanup;
}
// 7.5 分配数据包
packet = av_packet_alloc();
if (!packet) {
fprintf(stderr, "无法分配数据包\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// ==================== 步骤 8: 读取并解码输入帧 ====================
// 读取文件中的所有包
while (av_read_frame(inputFormatCtx, packet) >= 0) {
// 只处理视频流
if (packet->stream_index == videoStreamIndex) {
// 8.1 发送包到解码器
ret = avcodec_send_packet(decoderCtx, packet);
if (ret < 0) {
fprintf(stderr, "发送包到解码器失败\n");
av_packet_unref(packet);
continue;
}
// 8.2 从解码器接收帧
while (ret >= 0) {
ret = avcodec_receive_frame(decoderCtx, inputFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
fprintf(stderr, "解码错误\n");
goto cleanup;
}
// ==================== 步骤 9: 处理帧 ====================
// 如果需要缩放或格式转换
if (swsCtx) {
// 缩放并格式转换
sws_scale(
swsCtx,
(const uint8_t *const *)inputFrame->data,
inputFrame->linesize,
0, // 源切片位置
inputHeight, // 源高度
outputFrame->data, // 目标数据
outputFrame->linesize // 目标行大小
);
} else {
// 不需要缩放,直接引用
// 注意: 这种情况需要更复杂的处理,通常简化为使用 sws_scale
av_frame_ref(outputFrame, inputFrame);
}
// ==================== 步骤 10: 编码帧 ====================
// 10.1 发送帧到编码器
ret = avcodec_send_frame(encoderCtx, outputFrame);
if (ret < 0) {
fprintf(stderr, "发送帧到编码器失败\n");
av_frame_unref(outputFrame);
break;
}
// 10.2 从编码器接收包
while (ret >= 0) {
ret = avcodec_receive_packet(encoderCtx, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
fprintf(stderr, "编码错误\n");
goto cleanup;
}
// 10.3 写入包到输出文件
// 调整时间戳
av_packet_rescale_ts(
packet,
encoderCtx->time_base,
outputStream->time_base
);
packet->stream_index = outputStream->index;
// 写入包
ret = av_interleaved_write_frame(outputFormatCtx, packet);
if (ret < 0) {
fprintf(stderr, "写入包失败\n");
av_packet_unref(packet);
goto cleanup;
}
// 释放包
av_packet_unref(packet);
}
// 清理输出帧
av_frame_unref(outputFrame);
}
// 清理输入帧
av_frame_unref(inputFrame);
}
// 释放包
av_packet_unref(packet);
}
// ==================== 步骤 11: 刷新编码器 ====================
// 发送 NULL 帧以刷新编码器(输出延迟帧)
avcodec_send_frame(encoderCtx, NULL);
while (avcodec_receive_packet(encoderCtx, packet) == 0) {
// 写入剩余的包
av_packet_rescale_ts(packet, encoderCtx->time_base, outputStream->time_base);
packet->stream_index = outputStream->index;
av_interleaved_write_frame(outputFormatCtx, packet);
av_packet_unref(packet);
}
// ==================== 步骤 12: 写入文件尾 ====================
ret = av_write_trailer(outputFormatCtx);
if (ret < 0) {
fprintf(stderr, "写入文件尾失败\n");
goto cleanup;
}
printf("图片压缩成功!\n");
ret = 0;
// ==================== 清理资源 ====================
cleanup:
if (inputFrame) av_frame_free(&inputFrame);
if (outputFrame) av_frame_free(&outputFrame);
if (packet) av_packet_free(&packet);
if (swsCtx) sws_freeContext(swsCtx);
if (decoderCtx) avcodec_free_context(&decoderCtx);
if (encoderCtx) avcodec_free_context(&encoderCtx);
if (inputFormatCtx) avformat_close_input(&inputFormatCtx);
if (outputFormatCtx) {
if (!(outputFormatCtx->oformat->flags & AVFMT_NOFILE))
avio_closep(&outputFormatCtx->pb);
avformat_free_context(outputFormatCtx);
}
return ret;
}
3.2 使用示例
int main() {
// 压缩图片为 800x600,质量 80
int ret = compress_image(
"/sdcard/input.jpg",
"/sdcard/output.jpg",
800, // 宽度
600, // 高度
80 // 质量 (1-100)
);
if (ret == 0) {
printf("压缩成功!\n");
} else {
printf("压缩失败,错误码: %d\n", ret);
}
return 0;
}
4. 视频压缩实现
4.1 完整的视频压缩函数
/**
* 压缩视频
*
* @param inputPath 输入视频路径
* @param outputPath 输出视频路径
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @param targetBitrate 目标比特率 (bps)
* @param frameRate 帧率
* @param quality 质量 (1-51, 数值越小质量越好,H.264)
* @param progressCallback 进度回调函数
* @return 成功返回 0,失败返回负数
*/
typedef int (*ProgressCallback)(int current, int total);
int compress_video(const char *inputPath,
const char *outputPath,
int targetWidth,
int targetHeight,
int64_t targetBitrate,
int frameRate,
int quality,
ProgressCallback progressCallback) {
// ==================== 变量声明 ====================
AVFormatContext *inputFormatCtx = NULL;
AVFormatContext *outputFormatCtx = NULL;
AVCodecContext *decoderCtx = NULL;
AVCodecContext *encoderCtx = NULL;
SwsContext *swsCtx = NULL;
AVFrame *inputFrame = NULL;
AVFrame *outputFrame = NULL;
AVPacket *inputPacket = NULL;
AVPacket *outputPacket = NULL;
const AVCodec *decoder = NULL;
const AVCodec *encoder = NULL;
int videoStreamIndex = -1;
int ret = 0;
int64_t pts = 0; // 显示时间戳
// ==================== 步骤 1: 打开输入文件 ====================
ret = avformat_open_input(&inputFormatCtx, inputPath, NULL, NULL);
if (ret < 0) {
fprintf(stderr, "无法打开输入文件\n");
return ret;
}
ret = avformat_find_stream_info(inputFormatCtx, NULL);
if (ret < 0) {
fprintf(stderr, "无法获取流信息\n");
goto cleanup;
}
// 查找视频流
videoStreamIndex = av_find_best_stream(
inputFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0
);
if (videoStreamIndex < 0) {
fprintf(stderr, "未找到视频流\n");
ret = videoStreamIndex;
goto cleanup;
}
AVStream *inputStream = inputFormatCtx->streams[videoStreamIndex];
// ==================== 步骤 2: 初始化解码器 ====================
decoder = avcodec_find_decoder(inputStream->codecpar->codec_id);
if (!decoder) {
fprintf(stderr, "未找到解码器\n");
ret = AVERROR(EINVAL);
goto cleanup;
}
decoderCtx = avcodec_alloc_context3(decoder);
if (!decoderCtx) {
fprintf(stderr, "无法分配解码器上下文\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
ret = avcodec_parameters_to_context(decoderCtx, inputStream->codecpar);
if (ret < 0) {
fprintf(stderr, "无法复制解码器参数\n");
goto cleanup;
}
ret = avcodec_open2(decoderCtx, decoder, NULL);
if (ret < 0) {
fprintf(stderr, "无法打开解码器\n");
goto cleanup;
}
// 获取输入视频信息
int inputWidth = decoderCtx->width;
int inputHeight = decoderCtx->height;
AVRational inputTimeBase = decoderCtx->time_base;
int64_t duration = inputFormatCtx->duration;
int totalFrames = (int)(duration * frameRate / AV_TIME_BASE);
printf("输入视频信息:\n");
printf(" 尺寸: %dx%d\n", inputWidth, inputHeight);
printf(" 时长: %lld 秒\n", duration / AV_TIME_BASE);
printf(" 总帧数: %d\n", totalFrames);
// 计算目标尺寸(保持宽高比)
if (targetWidth > 0 && targetHeight > 0) {
double aspectRatio = (double)inputWidth / inputHeight;
if (targetWidth / aspectRatio <= targetHeight) {
targetHeight = (int)(targetWidth / aspectRatio);
} else {
targetWidth = (int)(targetHeight * aspectRatio);
}
// 确保是偶数(某些编码器要求)
targetWidth = targetWidth & ~1;
targetHeight = targetHeight & ~1;
} else {
targetWidth = inputWidth;
targetHeight = inputHeight;
}
printf("输出视频信息:\n");
printf(" 尺寸: %dx%d\n", targetWidth, targetHeight);
printf(" 比特率: %lld bps\n", targetBitrate);
printf(" 帧率: %d fps\n", frameRate);
// ==================== 步骤 3: 初始化编码器 (H.264) ====================
// 查找 H.264 编码器
encoder = avcodec_find_encoder_by_name("libx264");
if (!encoder) {
fprintf(stderr, "未找到 H.264 编码器,尝试其他编码器\n");
encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
}
if (!encoder) {
fprintf(stderr, "未找到编码器\n");
ret = AVERROR(EINVAL);
goto cleanup;
}
encoderCtx = avcodec_alloc_context3(encoder);
if (!encoderCtx) {
fprintf(stderr, "无法分配编码器上下文\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 设置编码参数
encoderCtx->width = targetWidth;
encoderCtx->height = targetHeight;
encoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // H.264 标准格式
encoderCtx->time_base = (AVRational){1, frameRate};
encoderCtx->framerate = (AVRational){frameRate, 1};
encoderCtx->bit_rate = targetBitrate;
encoderCtx->gop_size = 10; // GOP 大小(关键帧间隔)
encoderCtx->max_b_frames = 1; // B 帧数量
// H.264 特定参数
if (encoderCtx->codec_id == AV_CODEC_ID_H264) {
// 快速编码预设
av_opt_set(encoderCtx->priv_data, "preset", "ultrafast", 0);
// 调整质量 (CRF)
// 范围: 0-51, 数值越小质量越好,文件越大
// 18-28 是常用范围,23 是默认值
char crf[10];
snprintf(crf, sizeof(crf), "%d", quality);
av_opt_set(encoderCtx->priv_data, "crf", crf, 0);
}
// 打开编码器
ret = avcodec_open2(encoderCtx, encoder, NULL);
if (ret < 0) {
fprintf(stderr, "无法打开编码器\n");
goto cleanup;
}
// ==================== 步骤 4: 创建输出文件 ====================
ret = avformat_alloc_output_context2(&outputFormatCtx, NULL, NULL, outputPath);
if (ret < 0) {
fprintf(stderr, "无法分配输出格式上下文\n");
goto cleanup;
}
AVStream *outputStream = avformat_new_stream(outputFormatCtx, NULL);
if (!outputStream) {
fprintf(stderr, "无法创建输出流\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
ret = avcodec_parameters_from_context(outputStream->codecpar, encoderCtx);
if (ret < 0) {
fprintf(stderr, "无法复制编码器参数\n");
goto cleanup;
}
outputStream->time_base = encoderCtx->time_base;
ret = avio_open(&outputFormatCtx->pb, outputPath, AVIO_FLAG_WRITE);
if (ret < 0) {
fprintf(stderr, "无法打开输出文件\n");
goto cleanup;
}
ret = avformat_write_header(outputFormatCtx, NULL);
if (ret < 0) {
fprintf(stderr, "无法写入文件头\n");
goto cleanup;
}
// ==================== 步骤 5: 初始化缩放上下文 ====================
if (inputWidth != targetWidth || inputHeight != targetHeight ||
decoderCtx->pix_fmt != encoderCtx->pix_fmt) {
swsCtx = sws_getContext(
inputWidth, inputHeight, decoderCtx->pix_fmt,
targetWidth, targetHeight, encoderCtx->pix_fmt,
SWS_BICUBIC, NULL, NULL, NULL
);
if (!swsCtx) {
fprintf(stderr, "无法初始化缩放上下文\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
}
// ==================== 步骤 6: 分配帧和包 ====================
inputFrame = av_frame_alloc();
outputFrame = av_frame_alloc();
inputPacket = av_packet_alloc();
outputPacket = av_packet_alloc();
if (!inputFrame || !outputFrame || !inputPacket || !outputPacket) {
fprintf(stderr, "无法分配帧或包\n");
ret = AVERROR(ENOMEM);
goto cleanup;
}
// 设置输出帧参数
outputFrame->width = targetWidth;
outputFrame->height = targetHeight;
outputFrame->format = encoderCtx->pix_fmt;
ret = av_frame_get_buffer(outputFrame, 0);
if (ret < 0) {
fprintf(stderr, "无法分配输出帧缓冲区\n");
goto cleanup;
}
// ==================== 步骤 7: 主处理循环 ====================
int frameCount = 0;
while (av_read_frame(inputFormatCtx, inputPacket) >= 0) {
if (inputPacket->stream_index == videoStreamIndex) {
// 解码
ret = avcodec_send_packet(decoderCtx, inputPacket);
if (ret < 0) {
av_packet_unref(inputPacket);
continue;
}
while (ret >= 0) {
ret = avcodec_receive_frame(decoderCtx, inputFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
fprintf(stderr, "解码错误\n");
goto cleanup;
}
// 缩放和格式转换
if (swsCtx) {
sws_scale(
swsCtx,
(const uint8_t *const *)inputFrame->data,
inputFrame->linesize,
0, inputHeight,
outputFrame->data,
outputFrame->linesize
);
} else {
av_frame_ref(outputFrame, inputFrame);
}
// 设置时间戳
outputFrame->pts = pts;
// 编码
ret = avcodec_send_frame(encoderCtx, outputFrame);
if (ret < 0) {
fprintf(stderr, "发送帧到编码器失败\n");
av_frame_unref(outputFrame);
break;
}
while (ret >= 0) {
ret = avcodec_receive_packet(encoderCtx, outputPacket);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
fprintf(stderr, "编码错误\n");
goto cleanup;
}
// 写入包
av_packet_rescale_ts(
outputPacket,
encoderCtx->time_base,
outputStream->time_base
);
outputPacket->stream_index = outputStream->index;
ret = av_interleaved_write_frame(outputFormatCtx, outputPacket);
if (ret < 0) {
fprintf(stderr, "写入包失败\n");
av_packet_unref(outputPacket);
goto cleanup;
}
av_packet_unref(outputPacket);
}
av_frame_unref(outputFrame);
pts++;
// 进度回调
frameCount++;
if (progressCallback && frameCount % 10 == 0) {
progressCallback(frameCount, totalFrames);
}
}
av_frame_unref(inputFrame);
}
av_packet_unref(inputPacket);
}
// ==================== 步骤 8: 刷新编码器 ====================
avcodec_send_frame(encoderCtx, NULL);
while (avcodec_receive_packet(encoderCtx, outputPacket) == 0) {
av_packet_rescale_ts(outputPacket, encoderCtx->time_base, outputStream->time_base);
outputPacket->stream_index = outputStream->index;
av_interleaved_write_frame(outputFormatCtx, outputPacket);
av_packet_unref(outputPacket);
}
// ==================== 步骤 9: 写入文件尾 ====================
ret = av_write_trailer(outputFormatCtx);
if (ret < 0) {
fprintf(stderr, "写入文件尾失败\n");
goto cleanup;
}
printf("视频压缩成功!\n");
printf("处理帧数: %d\n", frameCount);
ret = 0;
// ==================== 清理资源 ====================
cleanup:
if (inputFrame) av_frame_free(&inputFrame);
if (outputFrame) av_frame_free(&outputFrame);
if (inputPacket) av_packet_free(&inputPacket);
if (outputPacket) av_packet_free(&outputPacket);
if (swsCtx) sws_freeContext(swsCtx);
if (decoderCtx) avcodec_free_context(&decoderCtx);
if (encoderCtx) avcodec_free_context(&encoderCtx);
if (inputFormatCtx) avformat_close_input(&inputFormatCtx);
if (outputFormatCtx) {
if (!(outputFormatCtx->oformat->flags & AVFMT_NOFILE))
avio_closep(&outputFormatCtx->pb);
avformat_free_context(outputFormatCtx);
}
return ret;
}
4.2 使用示例
// 进度回调函数
int progress_callback(int current, int total) {
double percent = (double)current / total * 100.0;
printf("进度: %.1f%% (%d/%d)\n", percent, current, total);
return 0;
}
int main() {
// 压缩视频
int ret = compress_video(
"/sdcard/input.mp4",
"/sdcard/output.mp4",
1280, // 目标宽度
720, // 目标高度
2000000, // 比特率 2Mbps
30, // 帧率 30fps
23, // CRF 质量 (18-28, 数值越小质量越好)
progress_callback
);
if (ret == 0) {
printf("压缩成功!\n");
} else {
printf("压缩失败,错误码: %d\n", ret);
}
return 0;
}
5. 高级功能
5.1 添加水印
/**
* 在视频上添加文字水印
*/
int add_watermark(const char *inputPath, const char *outputPath, const char *text) {
AVFormatContext *inputCtx = NULL;
AVFormatContext *outputCtx = NULL;
AVCodecContext *decoderCtx = NULL;
AVCodecContext *encoderCtx = NULL;
SwsContext *swsCtx = NULL;
AVFrame *frame = NULL;
AVPacket *packet = NULL;
int ret;
// 打开输入文件
ret = avformat_open_input(&inputCtx, inputPath, NULL, NULL);
if (ret < 0) return ret;
ret = avformat_find_stream_info(inputCtx, NULL);
if (ret < 0) goto cleanup;
// 查找视频流
int streamIndex = av_find_best_stream(inputCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (streamIndex < 0) {
ret = streamIndex;
goto cleanup;
}
// 初始化解码器和编码器 (类似前面的例子)
// ... (省略重复代码)
// 使用 libavfilter 添加水印
AVFilterGraph *filterGraph = avfilter_graph_alloc();
AVFilterContext *srcCtx = NULL;
AVFilterContext *sinkCtx = NULL;
// 创建滤镜描述
char filterDesc[512];
snprintf(filterDesc, sizeof(filterDesc),
"drawtext=text='%s':x=10:y=10:fontsize=24:fontcolor=white",
text);
// 初始化滤镜
// ... (滤镜初始化代码)
// 主循环:解码 -> 滤镜 -> 编码
// while (...) {
// // 解码
// avcodec_send_packet(decoderCtx, packet);
// avcodec_receive_frame(decoderCtx, frame);
//
// // 滤镜(添加水印)
// av_buffersrc_add_frame_flags(srcCtx, frame, AV_BUFFERSRC_FLAG_KEEP_REF);
// av_buffersink_get_frame(sinkCtx, frame);
//
// // 编码
// avcodec_send_frame(encoderCtx, frame);
// avcodec_receive_packet(encoderCtx, packet);
// av_interleaved_write_frame(outputCtx, packet);
// }
cleanup:
// 清理资源
if (inputCtx) avformat_close_input(&inputCtx);
if (outputCtx) avformat_free_context(outputCtx);
if (decoderCtx) avcodec_free_context(&decoderCtx);
if (encoderCtx) avcodec_free_context(&encoderCtx);
if (filterGraph) avfilter_graph_free(&filterGraph);
return ret;
}
5.2 视频裁剪
/**
* 裁剪视频(指定时间段)
*/
int trim_video(const char *inputPath,
const char *outputPath,
double startTime, // 开始时间(秒)
double duration) // 持续时间(秒){
AVFormatContext *inputCtx = NULL;
AVFormatContext *outputCtx = NULL;
int ret;
// 打开输入
ret = avformat_open_input(&inputCtx, inputPath, NULL, NULL);
if (ret < 0) return ret;
ret = avformat_find_stream_info(inputCtx, NULL);
if (ret < 0) goto cleanup;
// 跳转到开始时间
int64_t startTimestamp = (int64_t)(startTime * AV_TIME_BASE);
av_seek_frame(inputCtx, -1, startTimestamp, AVSEEK_FLAG_BACKWARD);
// 创建输出
ret = avformat_alloc_output_context2(&outputCtx, NULL, NULL, outputPath);
if (ret < 0) goto cleanup;
// 复制流
for (unsigned int i = 0; i < inputCtx->nb_streams; i++) {
AVStream *inStream = inputCtx->streams[i];
AVStream *outStream = avformat_new_stream(outputCtx, NULL);
avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
}
// 打开输出
ret = avio_open(&outputCtx->pb, outputPath, AVIO_FLAG_WRITE);
if (ret < 0) goto cleanup;
ret = avformat_write_header(outputCtx, NULL);
if (ret < 0) goto cleanup;
// 读取和写入包
AVPacket *packet = av_packet_alloc();
int64_t endTime = startTime + duration;
while (av_read_frame(inputCtx, packet) >= 0) {
AVStream *inStream = inputCtx->streams[packet->stream_index];
AVStream *outStream = outputCtx->streams[packet->stream_index];
// 检查时间戳
double packetTime = packet->pts * av_q2d(inStream->time_base);
if (packetTime > endTime) break;
if (packetTime < startTime) {
av_packet_unref(packet);
continue;
}
// 调整时间戳
av_packet_rescale_ts(packet, inStream->time_base, outStream->time_base);
packet->stream_index = outStream->index;
// 写入
av_interleaved_write_frame(outputCtx, packet);
av_packet_unref(packet);
}
// 写入尾
av_write_trailer(outputCtx);
cleanup:
if (packet) av_packet_free(&packet);
if (inputCtx) avformat_close_input(&inputCtx);
if (outputCtx) {
avio_closep(&outputCtx->pb);
avformat_free_context(outputCtx);
}
return ret;
}
6. 性能优化
6.1 使用硬件加速
/**
* 使用硬件编码器(如 MediaCodec)
*/
int init_hardware_encoder(AVCodecContext **encoderCtx, int width, int height) {
const AVCodec *encoder = NULL;
// Android: 使用 MediaCodec H264 编码器
#ifdef __ANDROID__
encoder = avcodec_find_encoder_by_name("h264_mediacodec");
#endif
// iOS: 使用 VideoToolbox
#ifdef __APPLE__
encoder = avcodec_find_encoder_by_name("h264_videotoolbox");
#endif
if (!encoder) {
fprintf(stderr, "未找到硬件编码器,使用软件编码\n");
encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
}
*encoderCtx = avcodec_alloc_context3(encoder);
if (!*encoderCtx) return AVERROR(ENOMEM);
// 设置参数
(*encoderCtx)->width = width;
(*encoderCtx)->height = height;
(*encoderCtx)->pix_fmt = encoder->pix_fmts ? encoder->pix_fmts[0] : AV_PIX_FMT_YUV420P;
(*encoderCtx)->time_base = (AVRational){1, 30};
(*encoderCtx)->bit_rate = 2000000;
// 打开编码器
return avcodec_open2(*encoderCtx, encoder, NULL);
}
6.2 多线程处理
/**
* 启用多线程编码
*/
void enable_multithreading(AVCodecContext *codecCtx, int threadCount) {
// 设置线程数
codecCtx->thread_count = threadCount;
codecCtx->thread_type = FF_THREAD_FRAME; // 帧级并行
// 或
// codecCtx->thread_type = FF_THREAD_SLICE; // 切片级并行
printf("启用 %d 个线程进行编码\n", threadCount);
}
// 使用示例
AVCodecContext *encoderCtx = avcodec_alloc_context3(encoder);
// ... 设置参数 ...
enable_multithreading(encoderCtx, 4); // 使用 4 个线程
avcodec_open2(encoderCtx, encoder, NULL);
6.3 内存复用
/**
* 复用帧和包,减少内存分配
*/
int process_video_optimized(const char *inputPath, const char *outputPath) {
// 一次性分配,循环复用
AVFrame *frame = av_frame_alloc();
AVPacket *packet = av_packet_alloc();
while (/* 读取帧 */) {
// 解码到复用的帧
avcodec_send_packet(decoderCtx, packet);
avcodec_receive_frame(decoderCtx, frame);
// 处理帧
// ...
// 清空帧以便复用
av_frame_unref(frame);
}
// 最后释放
av_frame_free(&frame);
av_packet_free(&packet);
return 0;
}
7. 错误处理
7.1 错误码处理
/**
* 错误处理宏
*/
#define CHECK_ERROR(cond, msg) \
do { \
if (cond) { \
fprintf(stderr, "错误: %s\n", msg); \
goto cleanup; \
} \
} while (0)
/**
* FFmpeg 错误处理
*/
void handle_ffmpeg_error(int ret) {
if (ret < 0) {
char errorBuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errorBuf, sizeof(errorBuf));
fprintf(stderr, "FFmpeg 错误: %s (错误码: %d)\n", errorBuf, ret);
}
}
// 使用示例
int compress_video_safe(const char *input, const char *output) {
int ret;
AVFormatContext *ctx = NULL;
ret = avformat_open_input(&ctx, input, NULL, NULL);
if (ret < 0) {
handle_ffmpeg_error(ret);
return ret;
}
// ... 其他操作
return 0;
}
7.2 资源清理模式
/**
* 使用 goto cleanup 模式确保资源释放
*/
int safe_compress_example(const char *input, const char *output) {
// 所有可能失败资源初始化为 NULL
AVFormatContext *inputCtx = NULL;
AVFormatContext *outputCtx = NULL;
AVCodecContext *decoderCtx = NULL;
AVCodecContext *encoderCtx = NULL;
AVFrame *frame = NULL;
AVPacket *packet = NULL;
int ret;
// 步骤 1
ret = avformat_open_input(&inputCtx, input, NULL, NULL);
CHECK_ERROR(ret < 0, "无法打开输入");
// 步骤 2
frame = av_frame_alloc();
CHECK_ERROR(!frame, "无法分配帧");
// ... 更多步骤 ...
ret = 0; // 成功
cleanup:
// 统一清理,即使出错也会执行
if (frame) av_frame_free(&frame);
if (packet) av_packet_free(&packet);
if (decoderCtx) avcodec_free_context(&decoderCtx);
if (encoderCtx) avcodec_free_context(&encoderCtx);
if (inputCtx) avformat_close_input(&inputCtx);
if (outputCtx) {
avio_closep(&outputCtx->pb);
avformat_free_context(outputCtx);
}
return ret;
}
7.3 常见错误及解决
/**
* 常见错误诊断
*/
void diagnose_ffmpeg_issues() {
// 1. 检查编解码器是否可用
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
printf("H.264 编码器不可用,需要重新编译 FFmpeg\n");
} else {
printf("H.264 编码器: %s\n", codec->name);
}
// 2. 检查像素格式支持
AVCodecContext *ctx = avcodec_alloc_context3(codec);
if (ctx) {
printf("支持的像素格式:\n");
for (const enum AVPixelFormat *fmt = codec->pix_fmts; *fmt != AV_PIX_FMT_NONE; fmt++) {
printf(" %s\n", av_get_pix_fmt_name(*fmt));
}
avcodec_free_context(&ctx);
}
// 3. 检查格式支持
av_register_all(); // 如果需要
AVOutputFormat *ofmt = av_guess_format("mp4", NULL, NULL);
if (ofmt) {
printf("MP4 格式: %s\n", ofmt->name);
}
}
总结
本文档提供了完整的 FFmpeg 图片和视频压缩实现指南,涵盖:
- 核心概念 - FFmpeg 架构和数据结构
- 图片压缩 - 完整的 JPEG 压缩实现
- 视频压缩 - H.264 视频编码实现
- 高级功能 - 水印、裁剪等
- 性能优化 - 硬件加速、多线程
- 错误处理 - 完善的错误处理机制
关键要点
- ✅ 使用
goto cleanup模式确保资源释放 - ✅ 检查所有 FFmpeg API 返回值
- ✅ 正确处理时间戳和帧率
- ✅ 使用
sws_scale进行图像缩放和格式转换 - ✅ 刷新编码器以输出延迟帧
- ✅ 启用硬件加速和多线程提升性能
编译配置
# CMakeLists.txt
find_library(avcodec-lib avcodec)
find_library(avformat-lib avformat)
find_library(avutil-lib avutil)
find_library(swscale-lib swscale)
target_link_libraries(your_library
${avcodec-lib}
${avformat-lib}
${avutil-lib}
${swscale-lib}
)
推荐学习路径
- 先掌握图片压缩(相对简单)
- 理解视频编码流程
- 学习高级功能(滤镜、裁剪)
- 实践性能优化
- 处理各种边界情况
祝你在 FFmpeg 开发中取得成功!🎉