V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wodong
V2EX  ›  程序员

技术求教,大疆 h264 裸流缓存使用 ffmpeg 解析推送到 rtmp 服务器延迟有 10s

  •  1
     
  •   wodong · 2023-04-27 10:38:19 +08:00 · 2794 次点击
    这是一个创建于 577 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码流程,main 中循环读取一个 h264 裸流文件并写入管道,ffmpeg 在 read_packet()中读取管道内容存到缓存中,在 start()函数中解析视频流等信息,在通过 show()方法开始推流。 下面代码在拉流时有 10s 的延迟,不知道是思路不对还是哪里有问题。 使用管道的原因:大疆的 H264 裸流数据是通过一个回调给我 char *buf , size_t len 所以代码中使用管道模拟数据过来

    #ifndef FIFO_DEMO2_CPP
    #define FIFO_DEMO2_CPP
    
    #include <string>
    #include <iostream>
    
    extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libavformat/avio.h>
    #include <libavutil/file.h>
    #include <libavutil/time.h>
    #include <libavutil/avutil.h>
    #include <unistd.h>
    #include <libavutil/common.h>
    }
    #include <thread>
    
    #include <fcntl.h>
    
    #include <fstream>
    
    
    using namespace std;
    
    static int videoindex = -1;
    
    static bool is_exit = false;
    
    class FifoDemo2 {
    
      /*
      代码流程,main 中循环读取一个 h264 裸流文件并写入管道,ffmpeg 在 read_packet()中读取管道内容存到缓存中,在 start()函数中解析视频流等信息,在通过 show()方法开始推流。
      下面代码在拉流时有 10s 的延迟,不知道是思路不对还是哪里有问题。
      使用管道的原因:大疆的 H264 裸流数据是通过一个回调给我 char *buf , size_t len 所以代码中使用管道模拟数据过来
      
      */
        
        
    public:
    
        // @opaque  : 是由用户提供的参数,指向用户数据
        // @buf     : 作为 FFmpeg 的输入,此处由用户准备好 buf 中的数据
        // @buf_size: buf 的大小
        // @return  : 本次 IO 数据量
        static int read_packet(void* opaque, uint8_t* buf, int buf_size) {
            std::cout << "read_packet:start" << std::endl;
            int len = read(fd[0], buf, buf_size);
            std::cout << "read_packet:" << len << std::endl;
            return len;
        }
    
        FifoDemo2() {
            int ret = pipe(fd);
            // 设置非阻塞读
            // fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | O_NONBLOCK);
            av_register_all();
            avformat_network_init();
    
            // 1. 分配缓冲区
            buffer = (uint8_t*)av_malloc(buffer_size);
            if (!(i_fmt_ctx = avformat_alloc_context())) {
                ret = AVERROR(ENOMEM);
                printFFError(ret);
            }
    
            // 创建 AVIO
            avio_ctx = avio_alloc_context(buffer, buffer_size,
                0, NULL, &read_packet, NULL, NULL);
            // avio_ctx = avio_alloc_context(buffer, buffer_size,
            //     0, NULL, NULL, NULL, NULL);
            if (!avio_ctx) {
                ret = AVERROR(ENOMEM);
                printFFError(ret);
            }
    
            i_fmt_ctx->pb = avio_ctx;
            i_fmt_ctx->flags = AVFMT_FLAG_CUSTOM_IO;
    
    
            //创建输出上下文
            ret = avformat_alloc_output_context2(&octx, NULL, "flv", m_rtmp_url);
            if (ret < 0) {
                printFFError(ret);
                return;
            }
        }
    
        void Start() {
            int ret;
            ret = avformat_open_input(&i_fmt_ctx, NULL, NULL, NULL);
            if (ret < 0) {
                printFFError(ret);
                return;
            }
    
            ret = avformat_find_stream_info(i_fmt_ctx, NULL);
            if (ret < 0) {
                printFFError(ret);
                return;
            }
    
            av_dump_format(i_fmt_ctx, 0, nullptr, 0);
    
            int i;
            for (i = 0; i < i_fmt_ctx->nb_streams; i++) {
                //获取输入视频流
                AVStream* in_stream = i_fmt_ctx->streams[i];
                //为输出上下文添加音视频流(初始化一个音视频流容器)
                AVStream* out_stream = avformat_new_stream(octx, in_stream->codec->codec);
                if (!out_stream) {
                    printf("未能成功添加音视频流\n");
                    ret = AVERROR_UNKNOWN;
                }
                if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
                    out_stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
                }
                out_stream->codec->codec_id = AV_CODEC_ID_FLV1;
    
                ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
                if (ret < 0) {
                    printf("copy 编解码器上下文失败\n");
                }
                out_stream->codecpar->codec_tag = 0;
            }
            //找到视频流的位置
            for (i = 0; i < i_fmt_ctx->nb_streams; i++) {
                if (i_fmt_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                    videoindex = i;
                    break;
                }
            }
            av_dump_format(octx, 0, m_rtmp_url, 1);
    
            //
            //                   准备推流
            //
            //打开 IO
            ret = avio_open(&octx->pb, m_rtmp_url, AVIO_FLAG_WRITE);
            if (ret < 0) {
                printFFError(ret);
                return;
            }
    
            //写入头部信息
            ret = avformat_write_header(octx, 0);
            if (ret < 0) {
                printFFError(ret);
                return;
            }
    
            show();
    
            getchar();
        }
    
        static void show() {
            AVPacket pkt;
            int ret = 0;
            //推流每一帧数据
            //int64_t pts  [ pts*(num/den)  第几秒显示]
            //int64_t dts  解码时间 [P 帧(相对于上一帧的变化) I 帧(关键帧,完整的数据) B 帧(上一帧和下一帧的变化)]  有了 B 帧压缩率更高。
            //获取当前的时间戳  微妙
            long long start_time = av_gettime();
            long long frame_index = 0;
    
            while (true) {
                // std::cout << "while() 11111" << std::endl;
                if (is_exit) break;
    
                int ret;
                //输入输出视频流
                AVStream* in_stream, * out_stream;
                //获取解码前数据
                //这里注意一下 因为 MP4 和 flv 同样都是一种视频格式,它里面的视频数据都是 h264 编码的 nalu 片段,所以直接取出重新打包即可
                ret = av_read_frame(i_fmt_ctx, &pkt);
                if (ret < 0) {
                    break;
                }
                cout << "pts:" << pkt.pts << "\tdts:" << pkt.dts << endl;
                //没有显示时间(比如未解码的 H.264 )
                if (pkt.pts == AV_NOPTS_VALUE) {
                    //AVRational time_base:时基。通过该值可以把 PTS ,DTS 转化为真正的时间。
                    AVRational time_base = i_fmt_ctx->streams[videoindex]->time_base;
    
                    //计算两帧之间的时间
                    /*
                    r_frame_rate 帧率 通常是 24 、25fps
                    av_q2d 转化为 double 类型
                    通过帧率计算 1 秒多少帧 也是一帧应该显示多长时间
                    单位:微秒
                    */
                    int64_t calc_duration =
                        (double)AV_TIME_BASE / av_q2d(i_fmt_ctx->streams[videoindex]->r_frame_rate);
                    //配置参数
                    pkt.pts = (double)(frame_index * calc_duration) /
                        (double)(av_q2d(time_base) * AV_TIME_BASE);//微秒 /微秒
                    //编码时间等于显示时间
                    pkt.dts = pkt.pts;
                    pkt.duration =
                        (double)calc_duration / (double)(av_q2d(time_base) * AV_TIME_BASE);
                }
                //延时
                if (pkt.stream_index == videoindex) {
                    AVRational time_base = i_fmt_ctx->streams[videoindex]->time_base;
                    AVRational time_base_q = { 1, AV_TIME_BASE };
                    //计算视频播放时间
                    int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
                    //计算实际视频的播放时间
                    int64_t now_time = av_gettime() - start_time;
    
                    AVRational avr = i_fmt_ctx->streams[videoindex]->time_base;
                    // cout << avr.num << " " << avr.den << "  " << pkt.dts << "  " << pkt.pts << "   "
                    //     << pts_time << endl;
                    if (pts_time > now_time) {
                        //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
                        av_usleep((unsigned int)(pts_time - now_time));
                        // cout << "睡眠了" << endl;
                    }
                }
                in_stream = i_fmt_ctx->streams[pkt.stream_index];
                out_stream = octx->streams[pkt.stream_index];
    
                //计算延时后,重新指定时间戳
                pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                    (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
                pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                    (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
                pkt.duration = (int)av_rescale_q(pkt.duration, in_stream->time_base,
                    out_stream->time_base);
    
                pkt.pos = -1;
    
                if (pkt.stream_index == videoindex) {
                    printf("Send %lld video frames to output URL\n", frame_index);
                    frame_index++;
                }
                // Send 3123 video frames to output URL
                // 发送 H264 裸流:754761 90000  6795308  6795308   75503422
    
                // 回调数据 仅测试使用
                // callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
                // printf("发送 H264 裸流:%ld", pkt.pts);
                //向输出上下文发送(向地址推送)
                ret = av_interleaved_write_frame(octx, &pkt);
    
                if (ret < 0) {
                    printf("S 发送数据包出错\n");
                    break;
                }
                //释放
                av_packet_unref(&pkt);
            }
        }
    
        ~FifoDemo2() {
            //关闭输出上下文,这个很关键。
            if (octx != NULL)
                avio_close(octx->pb);
            //释放输出封装上下文
            if (octx != NULL)
                avformat_free_context(octx);
            octx = NULL;
            avformat_close_input(&i_fmt_ctx);
            if (avio_ctx)
                av_freep(&avio_ctx->buffer);
            avio_context_free(&avio_ctx);
    
    
        }
    
    private:
        static void printFFError(int ret) {
            char buf[1024] = { 0 };
            av_strerror(ret, buf, sizeof(buf) - 1);
            std::cout << buf << std::endl;
        }
    
    
    private:
        static AVFormatContext* i_fmt_ctx;
        static AVIOContext* avio_ctx;
        uint8_t* buffer;
        size_t buffer_size = 40960;
    
        // 输出上下文
        static AVFormatContext* octx;
    
        static constexpr const char* m_rtmp_url = "rtmp://192.168.199.254:11935/live/test2?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc";
    
        static int fd[2];
    
        static bool m_stat;
    };
    
    
    AVFormatContext* FifoDemo2::i_fmt_ctx;
    AVFormatContext* FifoDemo2::octx;
    int FifoDemo2::fd[2];
    bool FifoDemo2::m_stat;
    AVIOContext* FifoDemo2::avio_ctx;
    
    int main() {
    
        // ffmpeg -re -i ../3.h264 -c copy -f flv "rtmp://192.168.199.254:11935/live/test2?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc"
    
        const char* fileName = "../3.h264";
        ifstream ifs(fileName, ios::in | ios::binary);
        if (!ifs.is_open()) {
            cout << "打开文件失败" << endl;
            return -1;
        }
    
        FifoDemo2 fifoDemo2;
    
        char buf[40960] = { 0 };
        int len = 0;
    
        thread t([&]() {
            while (!ifs.eof()) {
                memset(buf, 0, sizeof(buf));
                ifs.read(buf, sizeof(buf));
                len = ifs.gcount();
                cout << "while" << endl;
                int len = write(fd[1], buf, bufLen);
            }
    
            is_exit = true;
            });
        fifoDemo2.Start();
        getchar();
    
        return 0;
    
    }
    
    
    
    
    #endif // FIFO_DEMO2_CPP
    
    
    25 条回复    2023-04-27 18:57:38 +08:00
    fengleiyidao
        1
    fengleiyidao  
       2023-04-27 11:15:50 +08:00   ❤️ 1
    代码太长了,没读。
    但根据我的经验,有可能是 buffer 太长或者哪里阻塞了
    wodong
        2
    wodong  
    OP
       2023-04-27 11:18:26 +08:00
    @fengleiyidao 我把 buf 放成 4096 或者 40960 两个都还是一样的延迟
    yugoal
        3
    yugoal  
       2023-04-27 11:24:23 +08:00
    之前使用大疆 MSDK 的时候也遇到延迟 10 多秒这个问题,后面发现把音频打开后延迟就正常了
    wodong
        4
    wodong  
    OP
       2023-04-27 11:33:13 +08:00
    @yugoal MSDK 我记得大疆是已经帮你封装好了
    fengleiyidao
        5
    fengleiyidao  
       2023-04-27 11:56:52 +08:00
    @wodong 再看看是不是哪个函数阻塞住了
    liuxu
        6
    liuxu  
       2023-04-27 12:09:35 +08:00   ❤️ 1
    对延迟有要求不要用 rtmp ,处理起来事太多,低延迟 rtmp 都是各种疯狂魔改版本
    1. 缩短关键帧距离
    2. 检查 rtmp 服务器收到的流延迟多少
    3. rtmp 服务器缓存多少秒
    4. 拉流端看看缓存有多少

    最后的延迟就是这些加起来一起的


    楼上加音频流延迟正常,大概率是播放器解码时会等待音视频流双流,由于没有音频流,导致等待超时后直接播放的视频流
    wodong
        7
    wodong  
    OP
       2023-04-27 13:32:42 +08:00
    @fengleiyidao 打印 log 的时候全都没有阻塞直接发出去了,就是拉流端等了 10s 左右才显示
    wodong
        8
    wodong  
    OP
       2023-04-27 13:34:32 +08:00
    @liuxu ok ,感谢 V 友。我试试看
    NessajCN
        9
    NessajCN  
       2023-04-27 13:49:13 +08:00
    你直接在终端里用 ffmpeg -re -i ../3.h264 -c copy -f flv "rtmp://192.168.199.254:11935/live/test2?secret=035c73f7-bb6b-4889-a715-d9eb2d1925cc" 推流也有 10s 延迟吗?
    wodong
        10
    wodong  
    OP
       2023-04-27 13:57:53 +08:00
    @NessajCN 这个在拉流端可以直接播放,延迟非常低
    loken2020
        11
    loken2020  
       2023-04-27 14:01:43 +08:00   ❤️ 1
    ffmpeg 命令行是由 ffmpeg.c ffmpeg_opt.c cmdutils.c 等几个文件构成的。
    我建议你二次开发 ffmpeg.c ,把他封装成线程调用。直接启动 main 函数,然后自定义 AVIO 传递数据给它。这属于线程间通信
    你也可以直接使用 ffmpeg 命令,但是那样会开进程,需要进程间通信来传递数据。

    线程通信比进程通信方便一些。

    最后推荐看我的《 FFmpeg 原理》
    wodong
        12
    wodong  
    OP
       2023-04-27 14:02:58 +08:00
    log 当中是发出了 200 帧 ,VLC 这边才解码成功,判断应该是那个参数有问题导致了拉流端解码延迟了
    https://i.postimg.cc/7YcK8CDB/Snipaste-2023-04-27-14-00-08.png
    NessajCN
        13
    NessajCN  
       2023-04-27 14:10:06 +08:00
    @wodong 如果直接推效果很好,为什么不直接开个子进程直接调用 ffmpeg 呢? 感觉你造了个现成轮子呀....
    wodong
        14
    wodong  
    OP
       2023-04-27 14:52:11 +08:00
    @NessajCN 以后还会要接入 OpenCV 做智能识别这一块
    NessajCN
        15
    NessajCN  
       2023-04-27 15:27:07 +08:00
    @wodong 那也是接收端的事,跟推流无关呀
    wodong
        16
    wodong  
    OP
       2023-04-27 15:28:00 +08:00
    @NessajCN 最后的智能识别还是会在我的代码中处理
    NessajCN
        17
    NessajCN  
       2023-04-27 15:34:55 +08:00   ❤️ 2
    @wodong 要识别的话,直接 opencv 提取帧然后输出结果。
    推流则是把帧流推去 rtmp 服务那里,这两者在我的理解里必然是两块不同的代码对吧?
    哪怕你是想把识别结果直接 mux 到画面里,那也是先用 opencv 嵌好,编码,再推出去,对不对?
    所以「推流」这个部分不管怎样都是可以单独开子进程的
    yugoal
        18
    yugoal  
       2023-04-27 16:03:14 +08:00
    @wodong 是封装了,但是 Android 如果不给录音权限的话,他延迟会达到 10s 左右
    wodong
        19
    wodong  
    OP
       2023-04-27 16:10:48 +08:00
    @yugoal 确实,第一条件同步是音频的时间基
    wodong
        20
    wodong  
    OP
       2023-04-27 16:11:38 +08:00
    @NessajCN 确实,我研究一下使用管道直接推出去
    hu8245
        21
    hu8245  
       2023-04-27 16:25:06 +08:00
    可能不是你的原因
    1. 确认 原始 h.264 的 gop size ,看一个 gop 的时长是多少(最有可能的原因)
    2. 如果没有重新编解码,应该是 gop 和 封装导致的 延迟
    wodong
        22
    wodong  
    OP
       2023-04-27 18:19:52 +08:00
    @hu8245 这个点确实没去了解他具体是多少,但是我给出请求要 5s 一个关键帧了还是差不多,会不会是因为他用的 GDR 编码的问题
    vsyf
        23
    vsyf  
       2023-04-27 18:39:46 +08:00
    你把 sleep 去掉应该就可以了,不过没有意义。
    wodong
        24
    wodong  
    OP
       2023-04-27 18:55:29 +08:00
    @vsyf 这个去掉确实没啥意义,测试到推流是直接推出去了,应该是播放器那边的问题
    wodong
        25
    wodong  
    OP
       2023-04-27 18:57:38 +08:00
    @loken2020 下午没看到,一直都听说了 ffmpeg.c 可以找到答案,但是一直没去那里探讨,感谢
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2845 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 15:04 · PVG 23:04 · LAX 07:04 · JFK 10:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.