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

2023-04-27 10:38:19 +08:00
 wodong

代码流程,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

2853 次点击
所在节点    程序员
25 条回复
hu8245
2023-04-27 16:25:06 +08:00
可能不是你的原因
1. 确认 原始 h.264 的 gop size ,看一个 gop 的时长是多少(最有可能的原因)
2. 如果没有重新编解码,应该是 gop 和 封装导致的 延迟
wodong
2023-04-27 18:19:52 +08:00
@hu8245 这个点确实没去了解他具体是多少,但是我给出请求要 5s 一个关键帧了还是差不多,会不会是因为他用的 GDR 编码的问题
vsyf
2023-04-27 18:39:46 +08:00
你把 sleep 去掉应该就可以了,不过没有意义。
wodong
2023-04-27 18:55:29 +08:00
@vsyf 这个去掉确实没啥意义,测试到推流是直接推出去了,应该是播放器那边的问题
wodong
2023-04-27 18:57:38 +08:00
@loken2020 下午没看到,一直都听说了 ffmpeg.c 可以找到答案,但是一直没去那里探讨,感谢

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/935849

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX