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

如何用ffmpeg h264_nvenc编码单帧并降低屏幕流延迟

问题描述

我尝试用ffmpeg的h264_nvenc编码器实现实时屏幕流,但鼠标操作和画面显示之间有5-6帧延迟(前5-6帧是黑屏)。需求是:要么用h264_nvenc编码单帧屏幕图像并获取有效帧(包),要么把延迟降到1-2帧,同时想了解其他即时获取图像的方法。

更新说明

后来发现延迟来自编码器和解码器的帧缓存,想问能不能彻底消除这类帧延迟?(已补充解码器初始化函数和解码函数)
我试过编解码器刷新方法,第一次能即时获取图像,但没法重复实现。

相关代码

编码函数

static void encode(AVCodecContext* enc_ctx, AVFrame* frame, AVPacket* pkt, AVPacket** new_packet)
{
    int ret;

    ret = avcodec_send_frame(enc_ctx, frame);
    if (ret < 0) {
        fprintf(stderr, "Error sending a frame for encoding\n");
        exit(1);
    }

    int size = 0;

    while (ret >= 0) {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
        {
            //printf("count: %i size: %i\n", count, size);
            return;
        }
        else if (ret < 0) {
            fprintf(stderr, "Error during encoding\n");
            exit(1);
        }
        size += pkt->size;

        *new_packet = av_packet_clone(pkt);
        
        av_packet_unref(pkt);
    }
}

FFmpeg编码器初始化函数

void InitializeFFmpegEncoder()
{
    const AVCodec* codec;
    int i, ret, x, y;
    FILE* f;

    // Encoder

    codec = avcodec_find_encoder_by_name("h264_nvenc"); // hevc_nvenc h264_nvenc
    if (!codec) {
        fprintf(stderr, "Codec '%s' not found\n", "hevc");
        exit(1);
    }

    /*codec = avcodec_find_encoder(AV_CODEC_ID_H265);
    if (!codec) {
        fprintf(stderr, "Codec '%s' not found\n", "hevc");
        exit(1);
    }*/
    cout << "Encoder codec name: " << codec->name << endl;

    cout << "1" << endl;

    enc_c = avcodec_alloc_context3(codec);
    if (!enc_c) {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }
    cout << "2" << endl;
    pkt = av_packet_alloc();
    if (!pkt)
        exit(1);

    cout << "3" << endl;

    /* put sample parameters */
    enc_c->bit_rate = 192000000;
    /* resolution must be a multiple of two */
    enc_c->width = 1920;
    enc_c->height = 1080;
    /* frames per second */
    enc_c->time_base = AVRational(1, 1);
    //enc_c->framerate = AVRational(60, 1);

    /* emit one intra frame every ten frames
     * check frame pict_type before passing frame
     * to encoder, if frame->pict_type is AV_PICTURE_TYPE_I
     * then gop_size is ignored and the output of encoder
     * will always be I frame irrespective to gop_size
     */
    enc_c->gop_size = 1;
    enc_c->max_b_frames = 0;
    enc_c->pix_fmt = AV_PIX_FMT_BGR0;
    enc_c->keyint_min = 0;

    if (codec->id == AV_CODEC_ID_H264)
    {
        //av_opt_set(enc_c->priv_data, "preset", "p1", 0);
        //av_opt_set(enc_c->priv_data, "tune", "ull", 0);
        //av_opt_set(enc_c->priv_data, "zerolatency", "1", 0);
        //av_opt_set(enc_c->priv_data, "preset", "p1", 0);
        //av_opt_set(enc_c->priv_data, "tune", "ull", 0);
        //av_opt_set(enc_c->priv_data, "strict_gop", "1", 0);
        //av_opt_set(enc_c->priv_data, "preset", "lossless", 0);
        //av_opt_set(enc_c->priv_data, "zerolatency", "1", 0);
    }

    cout << "4" << endl;

    /* open it */
    ret = avcodec_open2(enc_c, codec, NULL);
    if (ret < 0) {
        printf("Could not open codec: %s\n", av_make_error_string((char[64])(0), 64, ret));
        exit(1);
    }

    cout << "5" << endl;

    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }
    frame->format = AV_PIX_FMT_BGR0; // AV_PIX_FMT_YUV444P AV_PIX_FMT_ARGB
    frame->width = enc_c->width;
    frame->height = enc_c->height;

    cout << "6" << endl;

    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        fprintf(stderr, "Could not allocate the video frame data\n");
        exit(1);
    }

    cout << "7" << endl;
}

解码器初始化函数

int InitializeFFmpegDecoder()
{
    int ret;
    const AVCodec* decoder = NULL;
    enum AVHWDeviceType type;
    int i;

    decoder = avcodec_find_decoder_by_name("h264_cuvid"); // h264_cuvid hevc_cuvid
    if (!decoder) {
        fprintf(stderr, "Codec not found\n");
        exit(1);
    }

    cout << "Decoder name: " << decoder->name << endl;

    if (!(decoder_ctx = avcodec_alloc_context3(decoder)))
        return AVERROR(ENOMEM);
    decoder_ctx->get_format = ffmpeg_GetFormat;
    decoder_ctx->gop_size = 0;
    decoder_ctx->max_b_frames = 0;
    decoder_ctx->keyint_min = 0;
    decoder_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;

    //decoder_ctx->get_format = get_format;

    if ((ret = avcodec_open2(decoder_ctx, decoder, NULL)) < 0) {
        fprintf(stderr, "Failed to open codec for stream \n");
        return -1;
    }

    if (ctx == NULL)
    {
        ctx = sws_getContext(1920, 1080,
            AV_PIX_FMT_NV12, 1920, 1080, // AV_PIX_FMT_YUV420P AV_PIX_FMT_NV12
            AV_PIX_FMT_BGR0, 0, 0, 0, 0);
    }
}

解码函数

static void decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt, char* bgra_image)
{
    int ret;

    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0) {
        fprintf(stderr, "Error sending a packet for decoding\n");
        exit(1);
    }

    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }

        char* outData[1] = { bgra_image }; // RGB24 have one plane
        int outLinesize[1] = { 4 * 1920 }; // RGB stride
        sws_scale(ctx, frame->data, frame->linesize, 0, 1080, (uint8_t* const*)outData, outLinesize);

        //*new_frame = av_frame_clone(frame);

        //av_frame_ref()

        //break;
    }
}
解决方案

一、编码器配置优化(核心)

你目前注释掉了关键的低延迟参数,必须启用这些配置:

  1. 在编码器初始化的codec->id == AV_CODEC_ID_H264分支里,取消以下参数的注释:
    av_opt_set(enc_c->priv_data, "preset", "p1", 0); // p1是最快的预设,牺牲少量画质换速度
    av_opt_set(enc_c->priv_data, "tune", "ull", 0); // 针对超低延迟场景优化
    av_opt_set(enc_c->priv_data, "zerolatency", "1", 0); // 禁用编码器帧缓存
    av_opt_set(enc_c->priv_data, "strict_gop", "1", 0); // 严格遵循GOP大小,避免额外缓存
    
  2. 补充设置enc_c->framerate = AVRational(60, 1);,明确帧率让编码器精准调度,避免自动缓存帧对齐。
  3. 将enc_c->time_base改为与帧率匹配:enc_c->time_base = AVRational(1, 60);,避免时间戳导致的延迟。

二、解码器配置优化

  1. 确保解码器启用硬件加速的低延迟模式,在解码器初始化时添加:
    av_opt_set(decoder_ctx->priv_data, "low_delay", "1", 0);
    
  2. 解码函数里,收到帧后立即break,避免循环等待后续帧(低延迟场景下每帧应即时输出):
    修改解码函数的循环部分:
    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;
        else if (ret < 0) {
            fprintf(stderr, "Error during decoding\n");
            exit(1);
        }
    
        // 处理帧逻辑...
    
        break; // 立即退出循环,不等待更多帧
    }
    

三、编解码流程调整

  1. 编码后立即刷新编码器:每次调用encode函数后,调用avcodec_send_frame(enc_ctx, NULL)强制编码器输出所有缓存的帧,再调用avcodec_receive_packet获取剩余包。修改后的编码函数末尾补充:
    // 发送NULL帧触发编码器刷新
    ret = avcodec_send_frame(enc_ctx, NULL);
    while (ret >= 0) {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            break;
        else if (ret < 0) {
            fprintf(stderr, "Error flushing encoder\n");
            exit(1);
        }
        size += pkt->size;
        *new_packet = av_packet_clone(pkt);
        av_packet_unref(pkt);
    }
    
  2. 避免重复初始化编解码器,整个流程只初始化一次,减少启动延迟。

四、单帧编码与即时获取

如果只需要编码单帧并立即获取有效包,除上述配置外,还要:

  • 确保每帧都是I帧(已设置gop_size=1,满足要求)
  • 编码时直接发送当前屏幕帧,然后立即刷新编码器,确保输出对应包,不被缓存。

五、其他即时获取图像的方法

  1. 直接内存拷贝:无需压缩,直接将屏幕帧拷贝到目标内存,零延迟但数据量极大(192010804=8MB/帧,60帧达480MB/s)。
  2. 无损压缩编码:使用h264_nvenc的lossless预设,结合低延迟参数,兼顾压缩率与实时性。
  3. 系统级帧采集:用DXGI/Windows Graphics Capture直接从系统获取屏幕帧,比传统GDI截图更快,减少采集延迟后再配合NVENC编码。

关于彻底消除帧延迟

理论上无法做到绝对零延迟,硬件编解码本身会有1-2帧的处理耗时,但通过上述优化可将延迟控制在1-2帧以内。前几帧黑屏是因为编码器初始化需要输出第一个I帧,优化后可让第一帧即时输出,避免黑屏等待。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 11:54:25