FFmpeg 原生 API 压缩图片和视频完全指南

6 阅读21分钟

FFmpeg 原生 API 压缩图片和视频完全指南

目录

  1. FFmpeg 核心概念
  2. 开发环境配置
  3. 图片压缩实现
  4. 视频压缩实现
  5. 高级功能
  6. 性能优化
  7. 错误处理

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 图片和视频压缩实现指南,涵盖:

  1. 核心概念 - FFmpeg 架构和数据结构
  2. 图片压缩 - 完整的 JPEG 压缩实现
  3. 视频压缩 - H.264 视频编码实现
  4. 高级功能 - 水印、裁剪等
  5. 性能优化 - 硬件加速、多线程
  6. 错误处理 - 完善的错误处理机制

关键要点

  • ✅ 使用 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}
)

推荐学习路径

  1. 先掌握图片压缩(相对简单)
  2. 理解视频编码流程
  3. 学习高级功能(滤镜、裁剪)
  4. 实践性能优化
  5. 处理各种边界情况

祝你在 FFmpeg 开发中取得成功!🎉