FFmpeg实战:从零构建基于libavcodec的现代视频转码器

如果你曾经尝试过用FFmpeg命令行工具处理视频,可能会觉得它功能强大但不够灵活。当我们需要在应用程序中嵌入视频处理能力时,直接调用FFmpeg的库才是真正的解决方案。今天,我将带你深入FFmpeg的核心编码解码库——libavcodec,手把手教你如何用它的现代API构建一个完整的视频转码器。

我在实际项目中遇到过这样的需求:需要将用户上传的各种格式视频统一转换为HEVC编码,同时保持尽可能高的质量并控制文件大小。最初我尝试用命令行调用,但发现性能瓶颈明显,内存占用高,而且错误处理困难。后来转向libavcodec API,虽然学习曲线陡峭,但一旦掌握,就能实现高度定制化的视频处理流程。

1. 理解libavcodec的核心架构

libavcodec是FFmpeg项目中负责编码和解码的核心组件,它支持数百种音视频编解码格式。但很多人第一次接触时会感到困惑:为什么有那么多相似的结构体和函数?它们之间到底是什么关系?

1.1 关键数据结构解析

在libavcodec的世界里,有几个核心结构体你需要彻底理解:

AVCodec vs AVCodecContext

这是最容易混淆的一对概念。简单来说,AVCodec是编解码器的描述符,而AVCodecContext是编解码器的运行时状态。

// AVCodec - 编解码器的静态描述
typedef struct AVCodec {
    const char *name;          // 编解码器名称,如"libx264"
    const char *long_name;     // 完整名称
    enum AVMediaType type;     // 媒体类型:视频、音频等
    enum AVCodecID id;         // 编解码器ID
    // ... 其他字段和函数指针
} AVCodec;

// AVCodecContext - 编解码器的运行时上下文
typedef struct AVCodecContext {
    const struct AVCodec *codec;  // 关联的AVCodec
    enum AVCodecID codec_id;      // 编解码器ID
    int bit_rate;                 // 比特率
    int width, height;            // 视频宽高
    // ... 数十个配置参数
} AVCodecContext;

想象一下,AVCodec就像是汽车的说明书,告诉你这辆车有什么功能、能跑多快。而AVCodecContext则是你实际驾驶的那辆车,包含了当前的速度、油量、位置等实时状态。

AVPacket与AVFrame的差异

另一个关键区别是AVPacket和AVFrame:

特性AVPacketAVFrame
数据状态压缩后的编码数据解码后的原始数据
存储内容编码后的字节流YUV/RGB像素数据或PCM音频
时间信息包含pts/dts包含pts、duration等
引用计数支持引用计数支持引用计数
典型用途从文件读取、写入文件处理、显示、滤镜输入

注意:在FFmpeg的现代API中,AVPacket和AVFrame都应该使用引用计数管理。这意味着你不应该直接复制它们的数据,而是使用av_packet_ref()和av_frame_ref()来增加引用计数。

1.2 新旧API的演变与选择

如果你搜索过FFmpeg的示例代码,可能会发现两种不同的API风格。旧API如avcodec_decode_video2()和avcodec_encode_video2()在FFmpeg 3.1之后被标记为废弃,取而代之的是更清晰的send/receive API。

为什么需要新的API?旧API存在几个问题:

  1. 状态混乱:解码器内部状态不透明
  2. 内存管理复杂:需要手动处理缓冲区
  3. B帧处理困难:延迟帧的处理逻辑复杂

新的send/receive API采用了生产者-消费者模型,让数据流更加清晰:

// 旧API - 单次调用完成解码
int ret = avcodec_decode_video2(codec_ctx, frame, &got_frame, packet);

// 新API - 分离输入输出
// 发送压缩数据到解码器
ret = avcodec_send_packet(codec_ctx, packet);
// 从解码器获取解码后的帧
while (ret >= 0) {
    ret = avcodec_receive_frame(codec_ctx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        break;
    // 处理frame
}

这种分离的设计让代码逻辑更清晰,也更容易处理复杂的编解码场景。

2. 构建H.264到HEVC转码器的完整实现

现在让我们进入实战环节。我将展示一个完整的视频转码器实现,将H.264视频转换为HEVC编码。这个例子涵盖了从文件读取到编码输出的完整流程。

2.1 环境准备与初始化

首先,我们需要设置开发环境。确保你的系统已经安装了FFmpeg开发库:

# Ubuntu/Debian
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev

# macOS with Homebrew
brew install ffmpeg

# 编译命令示例
gcc -o transcoder transcoder.c -lavcodec -lavformat -lavutil -lswscale -lm

在代码开始处,我们需要包含必要的头文件并初始化FFmpeg:

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "Usage: %s <input file> <output file>\n", argv[0]);
        return 1;
    }
    
    const char *input_filename = argv[1];
    const char *output_filename = argv[2];
    
    // 注册所有编解码器和格式
    av_register_all();  // 注意:FFmpeg 4.0+中已废弃,但为了兼容性保留
    
    // 后续代码...
}

提示:在FFmpeg 4.0及以上版本中,av_register_all()函数已被标记为废弃,因为现在编解码器和复用器/解复用器会自动注册。但为了向后兼容,我们仍然包含它。

2.2 解码器初始化与配置

初始化阶段需要完成几个关键步骤:打开输入文件、查找视频流、创建解码器上下文。

AVFormatContext *input_ctx = NULL;
AVCodecContext *decoder_ctx = NULL;
AVCodec *decoder = NULL;
int video_stream_index = -1;

// 1. 打开输入文件
if (avformat_open_input(&input_ctx, input_filename, NULL, NULL) < 0) {
    fprintf(stderr, "无法打开输入文件: %s\n", input_filename);
    return 1;
}

// 2. 获取流信息
if (avformat_find_stream_info(input_ctx, NULL) < 0) {
    fprintf(stderr, "无法获取流信息\n");
    avformat_close_input(&input_ctx);
    return 1;
}

// 3. 查找视频流
for (int i = 0; i < input_ctx->nb_streams; i++) {
    if (input_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream_index = i;
        break;
    }
}

if (video_stream_index == -1) {
    fprintf(stderr, "未找到视频流\n");
    avformat_close_input(&input_ctx);
    return 1;
}

// 4. 获取解码器并创建解码器上下文
AVCodecParameters *codecpar = input_ctx->streams[video_stream_index]->codecpar;
decoder = avcodec_find_decoder(codecpar->codec_id);
if (!decoder) {
    fprintf(stderr, "未找到对应的解码器\n");
    avformat_close_input(&input_ctx);
    return 1;
}

decoder_ctx = avcodec_alloc_context3(decoder);
if (!decoder_ctx) {
    fprintf(stderr, "无法分配解码器上下文\n");
    avformat_close_input(&input_ctx);
    return 1;
}

// 5. 将流参数复制到解码器上下文
if (avcodec_parameters_to_context(decoder_ctx, codecpar) < 0) {
    fprintf(stderr, "无法复制编解码器参数\n");
    avcodec_free_context(&decoder_ctx);
    avformat_close_input(&input_ctx);
    return 1;
}

// 6. 打开解码器
if (avcodec_open2(decoder_ctx, decoder, NULL) < 0) {
    fprintf(stderr, "无法打开解码器\n");
    avcodec_free_context(&decoder_ctx);
    avformat_close_input(&input_ctx);
    return 1;
}

这里有几个关键点需要注意:

  1. avformat_open_input 不仅打开文件,还会读取文件头信息填充AVFormatContext
  2. avformat_find_stream_info 会进一步探测文件,获取更详细的流信息
  3. avcodec_parameters_to_context 是FFmpeg 3.1引入的新API,用于安全地复制编解码器参数

2.3 编码器设置与参数优化

创建编码器比解码器更复杂,因为我们需要手动设置大量参数。对于H.264到HEVC的转码,合理的参数设置对输出质量和性能至关重要。

AVCodecContext *encoder_ctx = NULL;
AVCodec *encoder = NULL;

// 1. 查找HEVC编码器
encoder = avcodec_find_encoder_by_name("libx265");
if (!encoder) {
    // 回退到通用的HEVC编码器
    encoder = avcodec_find_encoder(AV_CODEC_ID_HEVC);
}
if (!encoder) {
    fprintf(stderr, "未找到HEVC编码器\n");
    // 清理资源...
    return 1;
}

// 2. 创建编码器上下文
encoder_ctx = avcodec_alloc_context3(encoder);
if (!encoder_ctx) {
    fprintf(stderr, "无法分配编码器上下文\n");
    // 清理资源...
    return 1;
}

// 3. 设置编码参数
encoder_ctx->width = decoder_ctx->width;
encoder_ctx->height = decoder_ctx->height;
encoder_ctx->pix_fmt = decoder_ctx->pix_fmt;
encoder_ctx->time_base = (AVRational){1, 25};  // 假设25fps
encoder_ctx->framerate = (AVRational){25, 1};

// 比特率设置 - 根据分辨率动态调整
if (decoder_ctx->width * decoder_ctx->height > 1920 * 1080) {
    encoder_ctx->bit_rate = 8000000;  // 8 Mbps for 4K
} else if (decoder_ctx->width * decoder_ctx->height > 1280 * 720) {
    encoder_ctx->bit_rate = 4000000;  // 4 Mbps for 1080p
} else {
    encoder_ctx->bit_rate = 1500000;  // 1.5 Mbps for 720p
}

// 4. 设置GOP结构和B帧
encoder_ctx->gop_size = 250;  // 每250帧一个关键帧
encoder_ctx->max_b_frames = 3;  // 最多3个B帧

// 5. 设置档次和级别
av_opt_set(encoder_ctx->priv_data, "preset", "medium", 0);
av_opt_set(encoder_ctx->priv_data, "tune", "zerolatency", 0);

// 6. 打开编码器
AVDictionary *encoder_options = NULL;
av_dict_set(&encoder_options, "x265-params", "log-level=warning", 0);

if (avcodec_open2(encoder_ctx, encoder, &encoder_options) < 0) {
    fprintf(stderr, "无法打开编码器\n");
    av_dict_free(&encoder_options);
    // 清理资源...
    return 1}

编码器参数设置需要根据具体需求调整。下面是一个参数配置参考表:

参数推荐值说明
比特率动态计算根据分辨率调整,4K: 8-12Mbps, 1080p: 4-6Mbps, 720p: 1.5-3Mbps
GOP大小10-250值越小,seek越快但文件越大;直播场景建议10-30
B帧数量2-3提高压缩率,但增加解码复杂度
预设(preset)medium平衡编码速度和质量,可选ultrafast到veryslow
调优(tune)zerolatency低延迟场景使用,其他选项:psnr, ssim, grain

2.4 核心转码循环的实现

转码循环是整个程序的核心,它负责读取、解码、处理、编码和写入数据。这个循环需要正确处理各种边界情况和错误状态。

AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
AVFrame *filtered_frame = av_frame_alloc();
int ret = 0;

if (!packet || !frame || !filtered_frame) {
    fprintf(stderr, "无法分配数据包或帧\n");
    // 清理资源...
    return 1;
}

// 创建输出格式上下文
AVFormatContext *output_ctx = NULL;
avformat_alloc_output_context2(&output_ctx, NULL, NULL, output_filename);
if (!output_ctx) {
    fprintf(stderr, "无法创建输出上下文\n");
    // 清理资源...
    return 1;
}

// 添加视频流到输出
AVStream *out_stream = avformat_new_stream(output_ctx, encoder);
if (!out_stream) {
    fprintf(stderr, "无法创建输出流\n");
    // 清理资源...
    return 1;
}

// 复制编码器参数到输出流
if (avcodec_parameters_from_context(out_stream->codecpar, encoder_ctx) < 0) {
    fprintf(stderr, "无法复制编码器参数\n");
    // 清理资源...
    return 1;
}

// 写入文件头
if (avformat_write_header(output_ctx, NULL) < 0) {
    fprintf(stderr, "无法写入文件头\n");
    // 清理资源...
    return 1;
}

// 主转码循环
while (av_read_frame(input_ctx, packet) >= 0) {
    // 只处理视频流
    if (packet->stream_index != video_stream_index) {
        av_packet_unref(packet);
        continue;
    }
    
    // 发送数据包到解码器
    ret = avcodec_send_packet(decoder_ctx, packet);
    if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
        fprintf(stderr, "发送数据包到解码器失败: %s\n", av_err2str(ret));
        break;
    }
    
    av_packet_unref(packet);
    
    // 接收解码后的帧
    while (ret >= 0) {
        ret = avcodec_receive_frame(decoder_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            fprintf(stderr, "从解码器接收帧失败: %s\n", av_err2str(ret));
            break;
        }
        
        // 这里可以添加帧处理逻辑,如缩放、滤镜等
        // ...
        
        // 发送帧到编码器
        frame->pts = av_rescale_q(frame->pts, 
                                 decoder_ctx->time_base, 
                                 encoder_ctx->time_base);
        
        ret = avcodec_send_frame(encoder_ctx, frame);
        if (ret < 0 && ret != AVERROR(EAGAIN)) {
            fprintf(stderr, "发送帧到编码器失败: %s\n", av_err2str(ret));
            av_frame_unref(frame);
            break;
        }
        
        av_frame_unref(frame);
        
        // 接收编码后的数据包
        while (ret >= 0) {
            AVPacket encoded_pkt;
            av_init_packet(&encoded_pkt);
            encoded_pkt.data = NULL;
            encoded_pkt.size = 0;
            
            ret = avcodec_receive_packet(encoder_ctx, &encoded_pkt);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                av_packet_unref(&encoded_pkt);
                break;
            } else if (ret < 0) {
                fprintf(stderr, "从编码器接收数据包失败: %s\n", av_err2str(ret));
                av_packet_unref(&encoded_pkt);
                break;
            }
            
            // 调整时间戳
            av_packet_rescale_ts(&encoded_pkt, 
                                encoder_ctx->time_base, 
                                out_stream->time_base);
            encoded_pkt.stream_index = out_stream->index;
            
            // 写入输出文件
            if (av_interleaved_write_frame(output_ctx, &encoded_pkt) < 0) {
                fprintf(stderr, "写入帧失败\n");
            }
            
            av_packet_unref(&encoded_pkt);
        }
    }
}

// 刷新解码器和编码器缓冲区
// ... (刷新逻辑)

这个循环中有几个关键的技术细节:

  1. 时间戳处理:解码器和编码器使用不同的时间基(time_base),需要正确转换
  2. 引用计数管理:每次使用完AVPacket和AVFrame后都要调用av_packet_unref()或av_frame_unref()
  3. 错误处理:EAGAIN不是错误,只是表示需要更多输入或输出

2.5 刷新缓冲区与资源清理

编解码器内部有缓冲区,处理完所有输入数据后,需要发送NULL来刷新这些缓冲区:

// 刷新解码器
avcodec_send_packet(decoder_ctx, NULL);
while (1) {
    ret = avcodec_receive_frame(decoder_ctx, frame);
    if (ret == AVERROR_EOF) {
        break;
    } else if (ret < 0) {
        break;
    }
    
    // 处理剩余的帧...
    // 编码并写入...
}

// 刷新编码器
avcodec_send_frame(encoder_ctx, NULL);
while (1) {
    AVPacket encoded_pkt;
    av_init_packet(&encoded_pkt);
    encoded_pkt.data = NULL;
    encoded_pkt.size = 0;
    
    ret = avcodec_receive_packet(encoder_ctx, &encoded_pkt);
    if (ret == AVERROR_EOF) {
        av_packet_unref(&encoded_pkt);
        break;
    } else if (ret < 0) {
        av_packet_unref(&encoded_pkt);
        break;
    }
    
    // 写入剩余的包...
    av_packet_rescale_ts(&encoded_pkt, encoder_ctx->time_base, out_stream->time_base);
    encoded_pkt.stream_index = out_stream->index;
    av_interleaved_write_frame(output_ctx, &encoded_pkt);
    av_packet_unref(&encoded_pkt);
}

// 写入文件尾
av_write_trailer(output_ctx);

// 清理所有资源
av_frame_free(&frame);
av_frame_free(&filtered_frame);
av_packet_free(&packet);
avcodec_free_context(&decoder_ctx);
avcodec_free_context(&encoder_ctx);
avformat_close_input(&input_ctx);
if (output_ctx && !(output_ctx->oformat->flags & AVFMT_NOFILE)) {
    avio_closep(&output_ctx->pb);
}
avformat_free_context(output_ctx);

3. 性能优化与高级特性

一个基础的转码器实现后,我们还需要考虑性能和功能优化。在实际项目中,我遇到过几个常见的性能瓶颈和解决方案。

3.1 多线程编码优化

libavcodec支持多种多线程模式,合理配置可以显著提升编码速度:

// 设置编码器多线程
encoder_ctx->thread_count = 0;  // 0表示自动检测CPU核心数
encoder_ctx->thread_type = FF_THREAD_FRAME;  // 帧级并行

// 或者使用slice级并行(对低延迟场景更友好)
// encoder_ctx->thread_type = FF_THREAD_SLICE;

// 对于解码器也可以设置多线程
decoder_ctx->thread_count = 4;  // 固定线程数
decoder_ctx->thread_type = FF_THREAD_FRAME;

不同的线程类型适用于不同场景:

  • FF_THREAD_FRAME:每帧独立编码,并行度高,但增加延迟
  • FF_THREAD_SLICE:每帧分片编码,延迟低,但并行度有限

3.2 硬件加速集成

对于需要高性能转码的场景,硬件加速是必不可少的。FFmpeg支持多种硬件编解码器:

// 检查可用的硬件解码器
enum AVHWDeviceType hw_type = av_hwdevice_find_type_by_name("cuda");
if (hw_type == AV_HWDEVICE_TYPE_NONE) {
    fprintf(stderr, "未找到CUDA硬件设备,回退到软件解码\n");
    // 使用软件解码
} else {
    // 配置硬件解码
    for (int i = 0; ; i++) {
        const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
        if (!config) {
            break;
        }
        if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
            config->device_type == hw_type) {
            // 创建硬件设备上下文
            AVBufferRef *hw_device_ctx = NULL;
            if (av_hwdevice_ctx_create(&hw_device_ctx, hw_type, NULL, NULL, 0) < 0) {
                fprintf(stderr, "无法创建硬件设备上下文\n");
            } else {
                decoder_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
            }
            break;
        }
    }
}

常见的硬件加速后端包括:

后端适用平台特点
CUDANVIDIA GPU性能优秀,支持编解码
VAAPIIntel/AMD Linux跨厂商支持
VideoToolboxmacOS/iOSApple平台原生支持
MediaCodecAndroid移动设备优化

3.3 内存管理与性能监控

大型视频处理容易遇到内存问题,合理的内存管理策略很重要:

// 自定义内存分配器(可选)
void *custom_alloc(void *opaque, size_t size) {
    void *ptr = aligned_alloc(64, size);  // 64字节对齐,提高缓存效率
    if (!ptr) {
        fprintf(stderr, "内存分配失败: %zu bytes\n", size);
    }
    return ptr;
}

void custom_free(void *opaque, void *ptr) {
    free(ptr);
}

// 设置自定义分配器
AVBufferPool *pool = av_buffer_pool_init2(1024 * 1024, NULL, custom_alloc, custom_free);

// 性能监控
int64_t start_time = av_gettime();
int frames_processed = 0;

// 在转码循环中...
frames_processed++;
if (frames_processed % 100 == 0) {
    int64_t current_time = av_gettime();
    double elapsed = (current_time - start_time) / 1000000.0;
    double fps = frames_processed / elapsed;
    printf("已处理 %d 帧,平均速度: %.2f fps\n", frames_processed, fps);
}

4. 错误处理与调试技巧

在实际开发中,完善的错误处理和调试机制能节省大量时间。以下是我在实践中总结的一些经验。

4.1 全面的错误检查

FFmpeg函数通常返回负数表示错误,但错误码的含义需要仔细处理:

int open_codec(AVCodecContext *ctx, AVCodec *codec, AVDictionary **options) {
    int ret = avcodec_open2(ctx, codec, options);
    if (ret < 0) {
        char error_buf[256];
        av_strerror(ret, error_buf, sizeof(error_buf));
        
        if (ret == AVERROR(EAGAIN)) {
            fprintf(stderr, "编解码器暂时不可用,请重试\n");
        } else if (ret == AVERROR(ENOMEM)) {
            fprintf(stderr, "内存不足,无法打开编解码器\n");
        } else if (ret == AVERROR_INVALIDDATA) {
            fprintf(stderr, "无效的编解码器参数\n");
        } else if (ret == AVERROR(ENOSYS)) {
            fprintf(stderr, "编解码器不支持此功能\n");
        } else {
            fprintf(stderr, "无法打开编解码器: %s (错误码: %d)\n", error_buf, ret);
        }
        
        // 尝试获取更详细的错误信息
        const char *error_desc = av_err2str(ret);
        fprintf(stderr, "详细错误: %s\n", error_desc);
        
        return ret;
    }
    return 0;
}

4.2 调试信息与日志

FFmpeg提供了详细的日志系统,合理设置日志级别可以帮助调试:

// 设置全局日志级别
av_log_set_level(AV_LOG_DEBUG);

// 自定义日志回调
void custom_log_callback(void *ptr, int level, const char *fmt, va_list vl) {
    if (level <= av_log_get_level()) {
        char message[1024];
        vsnprintf(message, sizeof(message), fmt, vl);
        
        // 添加时间戳
        time_t now = time(NULL);
        struct tm *tm_info = localtime(&now);
        char timestamp[64];
        strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
        
        // 根据级别输出不同颜色(Linux/macOS)
        const char *color = "";
        const char *level_str = "";
        
        switch (level) {
            case AV_LOG_PANIC:
            case AV_LOG_FATAL:
                color = "\033[1;31m";  // 红色
                level_str = "FATAL";
                break;
            case AV_LOG_ERROR:
                color = "\033[1;31m";  // 红色
                level_str = "ERROR";
                break;
            case AV_LOG_WARNING:
                color = "\033[1;33m";  // 黄色
                level_str = "WARN";
                break;
            case AV_LOG_INFO:
                color = "\033[1;32m";  // 绿色
                level_str = "INFO";
                break;
            case AV_LOG_VERBOSE:
                color = "\033[1;36m";  // 青色
                level_str = "VERBOSE";
                break;
            case AV_LOG_DEBUG:
                color = "\033[1;34m";  // 蓝色
                level_str = "DEBUG";
                break;
            default:
                color = "\033[0m";  // 默认
                level_str = "UNKNOWN";
        }
        
        fprintf(stderr, "%s[%s %s] %s\033[0m\n", 
                color, timestamp, level_str, message);
    }
}

// 设置自定义日志回调
av_log_set_callback(custom_log_callback);

4.3 性能分析与优化点

使用性能分析工具找出瓶颈:

// 添加性能统计
typedef struct {
    int64_t decode_time;
    int64_t encode_time;
    int64_t process_time;
    int frames_processed;
    int packets_processed;
} PerformanceStats;

PerformanceStats stats = {0};

// 在关键操作前后记录时间
int64_t start_decode = av_gettime();
ret = avcodec_send_packet(decoder_ctx, packet);
// ... 接收帧
int64_t end_decode = av_gettime();
stats.decode_time += (end_decode - start_decode);

// 处理帧...
int64_t start_encode = av_gettime();
ret = avcodec_send_frame(encoder_ctx, frame);
// ... 接收包
int64_t end_encode = av_gettime();
stats.encode_time += (end_encode - start_encode);

stats.frames_processed++;

// 定期输出统计信息
if (stats.frames_processed % 100 == 0) {
    double avg_decode = stats.decode_time / (double)stats.frames_processed / 1000.0;
    double avg_encode = stats.encode_time / (double)stats.frames_processed / 1000.0;
    double total_time = (stats.decode_time + stats.encode_time + stats.process_time) / 1000000.0;
    
    printf("性能统计:\n");
    printf("  总帧数: %d\n", stats.frames_processed);
    printf("  平均解码时间: %.2f ms/帧\n", avg_decode);
    printf("  平均编码时间: %.2f ms/帧\n", avg_encode);
    printf("  总处理时间: %.2f 秒\n", total_time);
    printf("  预估FPS: %.2f\n", stats.frames_processed / total_time);
}

通过这样的性能监控,你可以清楚地知道时间花在哪里,从而有针对性地优化。在我的经验中,常见的性能瓶颈包括:

  1. 内存拷贝:避免不必要的帧复制,使用引用计数
  2. 格式转换:减少YUV/RGB等格式转换次数
  3. 分辨率缩放:如果可能,在编码前一次性完成所有处理
  4. I/O操作:使用缓冲和异步I/O减少等待时间

掌握libavcodec的API需要时间和实践,但一旦熟练,你就能构建出高效、稳定的视频处理应用。记住,良好的错误处理、合理的资源管理和持续的性能优化是成功的关键。在实际项目中,建议先从简单的功能开始,逐步增加复杂度,同时保持代码的模块化和可测试性。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐