如何用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; } }
解决方案
一、编码器配置优化(核心)
你目前注释掉了关键的低延迟参数,必须启用这些配置:
- 在编码器初始化的
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大小,避免额外缓存 - 补充设置
enc_c->framerate = AVRational(60, 1);,明确帧率让编码器精准调度,避免自动缓存帧对齐。 - 将
enc_c->time_base改为与帧率匹配:enc_c->time_base = AVRational(1, 60);,避免时间戳导致的延迟。
二、解码器配置优化
- 确保解码器启用硬件加速的低延迟模式,在解码器初始化时添加:
av_opt_set(decoder_ctx->priv_data, "low_delay", "1", 0); - 解码函数里,收到帧后立即
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; // 立即退出循环,不等待更多帧 }
三、编解码流程调整
- 编码后立即刷新编码器:每次调用
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); } - 避免重复初始化编解码器,整个流程只初始化一次,减少启动延迟。
四、单帧编码与即时获取
如果只需要编码单帧并立即获取有效包,除上述配置外,还要:
- 确保每帧都是I帧(已设置
gop_size=1,满足要求) - 编码时直接发送当前屏幕帧,然后立即刷新编码器,确保输出对应包,不被缓存。
五、其他即时获取图像的方法
- 直接内存拷贝:无需压缩,直接将屏幕帧拷贝到目标内存,零延迟但数据量极大(192010804=8MB/帧,60帧达480MB/s)。
- 无损压缩编码:使用
h264_nvenc的lossless预设,结合低延迟参数,兼顾压缩率与实时性。 - 系统级帧采集:用DXGI/Windows Graphics Capture直接从系统获取屏幕帧,比传统GDI截图更快,减少采集延迟后再配合NVENC编码。
关于彻底消除帧延迟
理论上无法做到绝对零延迟,硬件编解码本身会有1-2帧的处理耗时,但通过上述优化可将延迟控制在1-2帧以内。前几帧黑屏是因为编码器初始化需要输出第一个I帧,优化后可让第一帧即时输出,避免黑屏等待。
内容的提问来源于stack exchange,提问作者CovER
相关产品推荐
相关产品推荐

