替换av_read_frame()以减少延迟

6
我正在使用ffmpeg实现一个(非常)低延迟视频流C++应用程序。客户端接收一个使用x264的零延迟预设编码的视频,因此无需缓冲。如这里所述,如果您使用av_read_frame()读取编码视频流的数据包,则始终会由于ffmpeg内部缓冲而至少有一个帧延迟。因此,当我在将第n+1帧发送到客户端后调用av_read_frame()时,该函数将返回第n帧。
通过设置AVFormatContext标志AVFMT_FLAG_NOPARSE | AVFMT_FLAG_NOFILLIN来摆脱此缓冲,如来源所建议的那样,会禁用数据包解析,因此会破坏解码,如来源中所述。
因此,我正在编写自己的数据包接收器和解析器。首先,以下是使用av_read_frame()的工作解决方案的相关步骤(包括一个帧延迟):
AVFormatContext *fctx;
AVCodecContext *cctx;
AVPacket *pkt;
AVFrame *frm;

//Initialization of AV structures
//…

//Main Loop
while(true){

    //Receive packet
    av_read_frame(fctx, pkt);

    //Decode:
    avcodec_send_packet(cctx, pkt);
    avcodec_receive_frame(cctx, frm);

    //Display frame
    //…
}

以下是我的解决方案,尽可能模仿了av_read_frame()的行为。我能够追踪到av_read_frame()的源代码,直到ff_read_packet(),但我找不到AVInputformat.read_packet()的源代码。
int tcpsocket;
AVCodecContext *cctx;
AVPacket *pkt;
AVFrame *frm;
uint8_t recvbuf[(int)10e5];
memset(recvbuf,0,10e5);
int pos = 0;

AVCodecParserContext * parser = av_parser_init(AV_CODEC_ID_H264);
parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
parser->flags |= PARSER_FLAG_USE_CODEC_TS;

//Initialization of AV structures and the tcpsocket
//…

//Main Loop
while(true){

    //Receive packet
    int length = read(tcpsocket, recvbuf, 10e5);
    if (length >= 0) {

        //Creating temporary packet
        AVPacket * tempPacket = new AVPacket;
        av_init_packet(tempPacket);
        av_new_packet(tempPacket, length);
        memcpy(tempPacket->data, recvbuf, length);
        tempPacket->pos = pos;
        pos += length;
        memset(recvbuf,0,length);

        //Parsing temporary packet into pkt
        av_init_packet(pkt);
        av_parser_parse2(parser, cctx,
            &(pkt->data), &(pkt->size),
            tempPacket->data, tempPacket->size,
            tempPacket->pts, tempPacket->dts, tempPacket->pos
            );

        pkt->pts = parser->pts;
        pkt->dts = parser->dts;
        pkt->pos = parser->pos;

        //Set keyframe flag
        if (parser->key_frame == 1 ||
            (parser->key_frame == -1 &&
            parser->pict_type == AV_PICTURE_TYPE_I))
            pkt->flags |= AV_PKT_FLAG_KEY;
        if (parser->key_frame == -1 && parser->pict_type == AV_PICTURE_TYPE_NONE && (pkt->flags & AV_PKT_FLAG_KEY))
            pkt->flags |= AV_PKT_FLAG_KEY;
        pkt->duration = 96000; //Same result as in av_read_frame()

        //Decode:
        avcodec_send_packet(cctx, pkt);
        avcodec_receive_frame(cctx, frm);
        //Display frame
        //…
    }
}

在两个解决方案中,在avcodec_send_packet()之前,我检查了结果数据包(pkt)的fields。据我所知,它们是相同的,唯一的区别可能是pkt->data的实际内容。我的解决方案可以很好地解码I帧,但P帧中的引用似乎已经损坏,导致出现严重的伪影和错误消息,例如“无效级别前缀”,“解码MB xx时出错”等。

如果有任何提示,我将非常感激。

编辑1:目前我已经开发出一个解决方法:在视频服务器中,在发送包含帧编码数据的数据包后,我发送一个只包含标记数据包开头和结尾的虚拟数据包。这样,我通过av_read_frame()推送实际的视频数据帧。我在av_frame_read()之后立即丢弃虚拟数据包。

编辑2:由rom1v解决here,如他对这个问题的评论所写。


3
关于AVInputformat.read_packet()函数指针,它将设置对应编解码器的read_packet()函数,最简单的确认方式是使用带有调试信息的FFmpeg进行编译(在'make'之前编辑ffbuild/config.mak文件,找到'STRIP = strip'并将其替换为'STRIP = echo',然后执行make和make install)。稍后在gdb上设置断点,运行您的代码并使用bt(回溯)命令触发所需的函数。我还注意到您同时使用了av_init_packetav_new_packet,但是av_new_packet也会调用av_init_packet,因此您不需要这样做。 - the kamilz
我遇到了同样的问题,av_read_frame()引入了1帧延迟:它调用(并阻塞)我的自定义avio_ctx->read_packet以接收更多数据,而它尚未消耗先前(完整的)数据包。你为你的“虚拟数据包”写入了什么数据? - rom1v
假设我使用 x264_encoder_encode(enc_ctx, &nals, &num_nals, &pic_in, &pic_out); 对一张图片进行编码。压缩后的数据在变量 nals 中。然后,在发送完整帧后(例如 for(int i=0;i<num_nals;++i){ write(newfd, nals[i].p_payload, nals[i].i_payload); }),我将帧的开头和结尾各写入 50 字节:write(sock, nals[0].p_payload, 50); write(sock, nals[num_nals - 1].p_payload + nals[num_nals -1].i_payload - 50, 50); - Chris_128
1
感谢您的回答。在您的原始帖子中,问题在于如果您设置了 PARSER_FLAG_COMPLETE_FRAMES,那么您需要负责仅将完整帧传递给 av_parser_parse2(如果您取消设置标志,则仍将获得1帧延迟)。我最终成功手动解析并将延迟减少了1帧:https://github.com/Genymobile/scrcpy/pull/646 - rom1v
好的观点。对我来说,它按照我之前的回答所描述的方式运作良好。感谢您分享您的解决方案,做得非常好! - Chris_128
我在尝试实现低延迟流媒体时遇到了同样的问题。问题是我无法修改服务器端。在这种情况下,有没有可能从av_read_frame中消除1帧延迟?谢谢。 - rkachach
1个回答

1

av_parser_parse2()不一定会一次性消耗你的tempPacket。你需要在另一个循环中调用它并检查其返回值,就像API文档中所述。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接