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

106 阅读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 开发中取得成功!🎉