You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

FFmpeg视频流内存与帧率异常问题求助

关于FFmpeg推流的内存飙升与播放卡顿问题解决思路

问题背景

首次使用FFmpeg生成视频流,测试环境为Nginx服务器+VLC播放客户端,流程是加载单张图片后逐帧处理、编码推流,遇到两个关联问题:

  • 无限制快速生成帧时,流看似实时播放,但电脑内存数秒内急剧飙升至数GB
  • 手动添加sleep_for将帧率限制到约40fps后,内存增长得到控制,但VLC出现间歇性卡顿:等待→播放数秒→停顿→恢复循环

已尝试的帧率设置

曾参考资料配置以下参数,试图标识流帧率为30fps并配置时序(90为FFmpeg采样率):

(*codec_context)->time_base = (AVRational){1, 30};
frame_yuv->pts = (1.0 / 30) * 90 * frame_count;

核心原因分析

  1. 内存飙升:无限制生成帧时,avcodec_send_frame会将未编码的帧存入编码器内部队列,编码器的编码速度远低于帧生成速度,导致队列无限膨胀,最终占用大量内存。
  2. 播放卡顿:固定20ms的sleep_for对应约50fps,与设置的30fps目标帧率不匹配,导致实际生成的帧时序混乱;同时sleep_for的精度有限,无法严格控制帧发送间隔,进一步加剧VLC解码时的时序冲突。

解决思路与技术建议

1. 严格控制帧生成速率(匹配目标帧率)

替换固定sleep为基于时间戳的动态延迟,确保每帧按30fps的间隔发送:

auto start_time = std::chrono::high_resolution_clock::now();
while (1) {
    // 计算当前帧的目标发送时间
    auto target_time = start_time + std::chrono::microseconds(static_cast<int>(frame_count * 1000000.0 / 30));
    auto now = std::chrono::high_resolution_clock::now();
    
    // 动态延迟,确保帧发送间隔严格匹配30fps
    if (now < target_time) {
        std::this_thread::sleep_for(target_time - now);
    }

    ProcessFrame(framebuffer_in, framebuffer, width, height);
    EncodeAndStreamFrame(codec_context, format_context, &framebuffer, width, height, sws_ctx);
    frame_count++;

    // 输出实际FPS
    auto end_time = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time);
    double fps = static_cast<double>(frame_count) / duration.count();
    fprintf(stderr, "\r%f fps", fps);
    fflush(stderr);
}

2. 修正编码器与时序配置

  • 显式设置编码器帧率,让编码器知晓目标帧率,优化内部队列管理:
// 在InitializeFFMPG函数中添加
(*codec_context)->framerate = (AVRational){30, 1};
  • 开启低延迟编码模式,减少编码延迟,避免队列积压:
// 在avcodec_open2之前添加
av_opt_set((*codec_context)->priv_data, "preset", "ultrafast", 0);
av_opt_set((*codec_context)->priv_data, "tune", "zerolatency", 0);
  • 修正PTS计算,用整数运算避免浮点误差,匹配RTMP的90kHz时钟标准:
// 替换原有的PTS计算行
frame_yuv->pts = frame_count * (90000 / 30);
// 或用FFmpeg内置的时间基转换函数(更通用)
frame_yuv->pts = av_rescale_q(frame_count, (AVRational){1, 30}, codec_context->time_base);

3. 优化资源复用,减少内存开销

原代码每次调用EncodeAndStreamFrame都重新分配frame_rgb和frame_yuv,频繁的内存分配释放会导致性能下降和内存碎片。将帧的分配移到初始化阶段,复用内存:

// main函数初始化部分添加
AVFrame* frame_rgb = av_frame_alloc();
frame_rgb->format = AV_PIX_FMT_RGB24;
frame_rgb->width = width;
frame_rgb->height = height;
if (av_image_alloc(frame_rgb->data, frame_rgb->linesize, width, height, AV_PIX_FMT_RGB24, 32) < 0) {
    std::cerr << "Failed to allocate RGB image buffer" << std::endl;
    return 1;
}

AVFrame* frame_yuv = av_frame_alloc();
frame_yuv->format = AV_PIX_FMT_YUV420P;
frame_yuv->width = width;
frame_yuv->height = height;
if (av_frame_get_buffer(frame_yuv, 32) < 0) {
    std::cerr << "Failed to allocate YUV frame buffer" << std::endl;
    av_frame_free(&frame_rgb);
    return 1;
}

// 修改EncodeAndStreamFrame的参数,传入已分配的帧
void EncodeAndStreamFrame(AVCodecContext* codec_context, AVFormatContext* format_context, 
    unsigned char** framebuffer, int width, int height, struct SwsContext* sws_ctx,
    AVFrame* frame_rgb, AVFrame* frame_yuv) {
    memcpy(frame_rgb->data[0], *framebuffer, width * height * 3);
    
    frame_yuv->pts = frame_count * (90000 / 30);
    ConvertRGBToYUV(frame_rgb, frame_yuv, sws_ctx);

    // 原有的编码推流逻辑...
    // 不再需要在函数内分配和释放frame_rgb、frame_yuv
}

// 循环内调用修改后的函数
EncodeAndStreamFrame(codec_context, format_context, &framebuffer, width, height, sws_ctx, frame_rgb, frame_yuv);

// 清理阶段释放帧
av_frame_free(&frame_rgb);
av_frame_free(&frame_yuv);

4. 错误处理优化

  • 检查av_interleaved_write_frame的返回值,如果发送失败(如网络拥塞),需处理重试或丢弃帧,避免未处理的数据包占用内存:
if (av_interleaved_write_frame(format_context, &pkt) < 0) {
    std::cerr << "Failed to write frame to stream" << std::endl;
    av_packet_unref(&pkt);
    // 可添加重试逻辑或记录日志
    break;
}

完整修正代码

extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libavutil/imgutils.h>
    #include <libavutil/opt.h>
    #include <libswscale/swscale.h>
}

#include <iostream>
#include <chrono>
#include <thread>
#include <fstream>
#include <cmath>

void InitializeFFMPG(AVCodecContext** codec_context, AVFormatContext** format_context, int width, int height) {
    avformat_network_init();
    
    avformat_alloc_output_context2(format_context, nullptr, "flv", "rtmp://localhost/live/stream");
    
    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        std::cerr << "avcodec_find_encoder err." << std::endl;
        return;
    }
    
    *codec_context = avcodec_alloc_context3(codec);
    if (!*codec_context) {
        std::cerr << "codec_context err." << std::endl;
        return;
    }
    
    (*codec_context)->width = width;
    (*codec_context)->height = height;
    (*codec_context)->pix_fmt = AV_PIX_FMT_YUV420P;
    (*codec_context)->time_base = (AVRational){1, 30};
    (*codec_context)->framerate = (AVRational){30, 1};
    
    // 开启低延迟编码
    av_opt_set((*codec_context)->priv_data, "preset", "ultrafast", 0);
    av_opt_set((*codec_context)->priv_data, "tune", "zerolatency", 0);
    
    if (avcodec_open2(*codec_context, codec, nullptr) < 0) {
        std::cerr << "Could not open codec" << std::endl;
        return;
    }
    
    AVStream* stream = avformat_new_stream(*format_context, codec);
    if (!stream) {
        std::cerr << "Could not create stream" << std::endl;
        return;
    }
    
    avcodec_parameters_from_context(stream->codecpar, *codec_context);
    
    if (avio_open(&(*format_context)->pb, "rtmp://localhost/live/stream", AVIO_FLAG_WRITE) < 0) {
        std::cerr << "Count not open output URL" << std::endl;
        return;
    }
    
    if (avformat_write_header(*format_context, nullptr) < 0) {
        std::cerr << "Could not write header" << std::endl;
        return;
    }
}

int frame_count = 0;

void ConvertRGBToYUV(AVFrame* frame_rgb, AVFrame* frame_yuv, struct SwsContext* sws_ctx) {
    sws_scale(sws_ctx, frame_rgb->data, frame_rgb->linesize, 0, frame_rgb->height, frame_yuv->data, frame_yuv->linesize);
}

void EncodeAndStreamFrame(AVCodecContext* codec_context, AVFormatContext* format_context, 
    unsigned char** framebuffer, int width, int height, struct SwsContext* sws_ctx,
    AVFrame* frame_rgb, AVFrame* frame_yuv) {
    memcpy(frame_rgb->data[0], *framebuffer, width * height * 3);
    
    frame_yuv->pts = frame_count * (90000 / 30);
    
    ConvertRGBToYUV(frame_rgb, frame_yuv, sws_ctx);

    if (avcodec_send_frame(codec_context, frame_yuv) < 0) {
        std::cerr << "Error sending YUV frame for encoding" << std::endl;
        return;
    }

    while (1) {
        AVPacket pkt = { 0 };
        av_packet_unref(&pkt);
        pkt.data = NULL;
        pkt.size = 0;

        int ret = avcodec_receive_packet(codec_context, &pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            std::cerr << "Error receiving encoded packet" << std::endl;
            break;
        }

        if (av_interleaved_write_frame(format_context, &pkt) < 0) {
            std::cerr << "Failed to write frame to stream" << std::endl;
            av_packet_unref(&pkt);
            break;
        }
        av_packet_unref(&pkt);
    }
}

void ProcessFrame(const unsigned char* in, unsigned char*& framebuffer, int width, int height) {
    float cosine = std::abs(std::cos(frame_count / 300.f));
    for (uint32_t i = 0; i < width * height * 3; ++i)
        framebuffer[i] = (unsigned char)((float)in[i] * cosine);
}

int main() {
    AVCodecContext *codec_context = nullptr;
    AVFormatContext *format_context = nullptr;
    int width = 320, height = 160;
    InitializeFFMPG(&codec_context, &format_context, width, height);
    
    if (!codec_context || !format_context) {
        std::cerr << "Initialization failed" << std::endl;
        return 1;
    }
    
    struct SwsContext* sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGB24,
                                                width, height, AV_PIX_FMT_YUV420P,
                                                SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!sws_ctx) {
        std::cerr << "Failed to create sws context" << std::endl;
        return 1;
    }

    unsigned char* framebuffer_in = new unsigned char[width*height*3];
    unsigned char* framebuffer = new unsigned char[width*height*3];
    memset(framebuffer, 0x0, width * height * 3);
    std::ifstream ifs("C:/Users/xxx/Downloads/ocean.ppm", std::ios::binary);
    if (!ifs.is_open()) {
        std::cerr << "Failed to open PPM file" << std::endl;
        return 1;
    }
    
    std::string header;
    ifs >> header;
    uint32_t w, h, bpc;
    ifs >> w >> h >> bpc;
    ifs.ignore();
    ifs.read((char*)framebuffer_in, w * h * 3);
    ifs.close();
    
    // 预分配帧内存,复用避免频繁分配
    AVFrame* frame_rgb = av_frame_alloc();
    if (!frame_rgb) {
        std::cerr << "Failed to allocate frame_rgb" << std::endl;
        return 1;
    }
    frame_rgb->format = AV_PIX_FMT_RGB24;
    frame_rgb->width = width;
    frame_rgb->height = height;
    if (av_image_alloc(frame_rgb->data, frame_rgb->linesize, width, height, AV_PIX_FMT_RGB24, 32) < 0) {
        std::cerr << "Failed to allocate RGB image buffer" << std::endl;
        av_frame_free(&frame_rgb);
        return 1;
    }
    
    AVFrame* frame_yuv = av_frame_alloc();
    if (!frame_yuv) {
        std::cerr << "Failed to allocate frame_yuv" << std::endl;
        av_frame_free(&frame_rgb);
        return 1;
    }
    frame_yuv->format = AV_PIX_FMT_YUV420P;
    frame_yuv->width = width;
    frame_yuv->height = height;
    if (av_frame_get_buffer(frame_yuv, 32) < 0) {
        std::cerr << "Failed to allocate YUV frame buffer" << std::endl;
        av_frame_free(&frame_rgb);
        av_frame_free(&frame_yuv);
        return 1;
    }
    
    auto start_time = std::chrono::high_resolution_clock::now();
    while (1) {
        auto target_time = start_time + std::chrono::microseconds(static_cast<int>(frame_count * 1000000.0 / 30));
        auto now = std::chrono::high_resolution_clock::now();
        
        if (now < target_time) {
            std::this_thread::sleep_for(target_time - now);
        }

        ProcessFrame(framebuffer_in, framebuffer, width, height);
        EncodeAndStreamFrame(codec_context, format_context, &framebuffer, width, height, sws_ctx, frame_rgb, frame_yuv);
        frame_count++;

        auto end_time = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::seconds>(end_time - start_time);
        double fps = static_cast<double>(frame_count) / duration.count();
        fprintf(stderr, "\r%f fps", fps);
        fflush(stderr);
    }

    // 清理资源
    delete[] framebuffer;
    delete[] framebuffer_in;
    av_frame_free(&frame_rgb);
    av_frame_free(&frame_yuv);
    sws_freeContext(sws_ctx);
    avcodec_close(codec_context);
    avformat_close_input(&format_context);
    avformat_free_context(format_context);
    avcodec_free_context(&codec_context);

    return 0;
}

内容的提问来源于stack exchange,提问作者user18490

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 01:53:14