当前位置: 首页 > article >正文

rtsp服务器逻辑

定时器逻辑:比如H264文件是每隔40ms发送一帧数据。aac文件每隔23ms发送一个音频帧数据。

在sink的子类中有aac和h264的sink,在两个子类的构造函数中需要添加它们各自的触发时间。调用的函数时runEvery(),将这两个触发时间设置到了TimerManager中,在TimerManager中有一个map类型的结构体mEvent:记录着触发时间。

在sink中需要create一个TimeEvent事件,这个是事件是时间到了触发,用来触发回调。设置回调函数:mTimerEvent->setTimeoutCallback(cbTimeout);就是定时的发送数据。

在cbTimeout回调函数中就是发送数据:

void Sink::handleTimeout() {
    MediaFrame* frame = mMediaSource->getFrameFromOutputQueue();
    if (!frame) {
        return;
    }
    this->sendFrame(frame);// 由具体子类实现发送逻辑

    mMediaSource->putFrameToInputQueue(frame);//将使用过的frame插入输入队列,插入输入队列以后,加入一个子线程task,从文件中读取数据再次将输入写入到frame
}

在TimerManager的构造函数中就是创建了一个定时器的文件描述符,将这个文件描述符添加到一个IO事件中,为这个IO事件添加一个回调函数,将这个IO事件添加到select模型中。这是一个定时器的管理者,用来定时触发读取事件。

设置的回调函数时readCallback,其中调用的是handleRead函数,一旦定时器触发,就会触发这个函数,函数中就是处理上述提到的TimerEvent,

当定时器触发会后,readCallback函数将从mEvent中移除已触发的事件,并调用modifyTimeOut函数重新设置下一个最近的定时事件。就相当于是处理完一个定时器事件之后重新设置新的时间戳的定时器。

void TimerManager::handleRead() {
    Timer::Timestamp timestamp = Timer::getCurTime(); // 获取当前时间
    if (!mTimers.empty() && !mEvents.empty()) { // 确保定时器和事件队列不为空
        std::multimap<Timer::Timestamp, Timer>::iterator it = mEvents.begin(); // 获取最早到期的定时器
        Timer timer = it->second; // 获取定时器对象
        int expire = timer.mTimestamp - timestamp; // 计算定时器到期剩余时间

        if (timestamp > timer.mTimestamp || expire == 0) { // 如果当前时间超过定时器的到期时间
            bool timerEventIsStop = timer.handleEvent(); // 处理定时器事件

            mEvents.erase(it); // 从事件队列中移除已处理的定时器

            if (timer.mRepeat) { // 如果是重复定时器
                if (timerEventIsStop) { // 如果定时器事件需要停止(例如,定时器任务完成后)
                    mTimers.erase(timer.mTimerId); // 从定时器管理器中移除
                } else { // 否则,重新安排定时器
                    timer.mTimestamp = timestamp + timer.mTimeInterval; // 设置下一个到期时间
                    mEvents.insert(std::make_pair(timer.mTimestamp, timer)); // 将新的定时器事件插入到事件队列中
                }
            } else { // 如果不是重复定时器
                mTimers.erase(timer.mTimerId); // 从定时器管理器中移除
            }
        }
    }
    modifyTimeout(); // 更新定时器的到期时间
}

定时器到期后触发的发送器发送逻辑

1.调用getFrameOutputQueue()函数取出一帧数据

2.调用函数sendFrame(frame)进行发送

3.调用putFrameToInputQueue(frame)函数,将使用过的frame插入输入队列中,插入输入队列之后,加入一个子线程task,从文件中读取数据再次将输入写入到frame中。

Main函数的主流程:

1.先创建了一个调度器EventScheduler,在构造函数中创建了Poller和创建了一个定时器管理者。

在定时器管理者构造函数中就是上面所提到的。

在调度器类中最主要的就是一个循环功能:只要不退出一直调用以下函数:

while (!mQuit) {
        handleTriggerEvents();
        mPoller->handleEvent();
    }

还有一些添加IO事件,移除IO事件,更新IO事件的函数;添加触发事件,移除触发事件的函数;添加定时器事件的函数。

2.创建了线程池,任务队列中的任务是从H264文件或者aac文件中读取数据,进行处理然后添加到mFrameOutputQueue队列中。

3.创建媒体会话管理者,在构造函数中啥也没干,这类中有几个函数,往MediaSessionManager中添加MediaSession,这里的MediaSession有一个名字叫做test,在MediaSession中有两路轨,一路是音频轨一路是视频轨,当然一个MediaSessionManager中肯定包含着很多的MediaSession。

4.设置RTSP服务器的IP地址和端口号

5.创建rtsp服务器,在构造函数中需要加MediaSessionManager,调度器和线程池以及IP和端口号传入当做参数。函数体中创建了socket文件描述符,为该文件描述符添加了IO事件并设置了回调函数。同时也设置了回调的关闭连接。“为该文件描述符添加了IO事件并设置了回调函数”这里设置的回调函数就是调用accept,然后创建一个rtsp连接,调用了RtspConnection构造函数,在这个类中主要处理的就是rtsp协议交互相关的东西。

比如有parseRequest函数用来解析客户端发送的RTSP请求,从请求中提取方法,URL,协议版本等信息。

handleReadBytes()函数:当有数据从客户端发送到服务器时,会调用此函数处理数据。判断是否是RTP over TCP,如果是调用handleRtpOverTcp()函数,否则解析RTSP请求并根据不同的RTSP方法执行相应的命令。

RTSP方法的处理:比如对OPTION请求,DESCRIBE请求,SETUP请求,PLAY请求的响应。

RTP和RTCP处理:通过UDP创建两者的实例,用于传输和接收流媒体数据,或者是通过TCP创建RTP实例等。

6.创建一个session也就是上面提到的MediaSession,在MediaSession构造函数中给每个MediaSession添加一个名字,在该服务器中就是test。我觉得这个类中关键的一个函数就是addSink(),这个函数的作用就是给指定的轨添加sink函数,在addSink()函数中有一个关键的操作就是给sink实例设置一个SessionCb,这里的回调函数才是真正将rtp数据包发送出去的函数。

其中还有addRtpInstance()函数以及remove函数,就是为轨道添加RTP实例,也是建立数据传输的媒介。

7.创建一个H264媒体资源,在构造函数中向线程池中添加4个任务,每个任务的处理过程是从mFrameInputQueue取第一帧数据frame,然后将H264文件中的数据拷贝到frame中,这里要检查并处理起始码,可能是00 00 01,也可能是00 00 00 01.然后再检查NALU类型,如果是0x09的话就跳过这帧处理下一帧,以上步骤如果成功的话将frame添加到mFrameOutputQueue中。

8.创建一个H264fileSink子类对象,需要将上面创建的H264媒体资源传给构造函数,在构造函数中设置了定时器的触发时间。这个子类中主要是重写了父类中的sendFrame()函数,在sendFrame函数中有两种发包的思路,一种是单NALU打包,一种是分片打包。在父类中还会调用父类的构造函数,在该构造函数中创建了一个定时器事件,同时设置了定时器事件的回调函数,表示定时器时间到后执行的函数。函数的逻辑就是时间到后发送帧数据。子类中的sendFrame函数中的关键函数在父类中:sendRtpPacket()函数,这个函数中就是调用了一个回调函数,那么是在哪里设置了这个回调函数呢?就是在接下来的addSink中设置的。

9.向session中添加sink实例,也就是调用了session中的addSink()函数,在MediaSession中有两个轨,我们就将H264的sink子类添加到TrackId0中,将aac的sink子类添加到TrackId1中。在addSink函数中就设置了sink中的回调函数。

那么整个流程是什么样的呢?首先我们设置了定时器,例如我们的H264文件的帧率是25帧的,那么就是一秒播放25帧,也就是40ms一帧数据。我们将这个数值设置到定时器管理者中,这个时候管理者到点就会触发回调函数也就是TimerManager::readCallback,也就是取出对应的定时器,执行定时器里面的事件。这里的定时器里面的事情是在sink构造函数中设置的 mTimerEvent->setTimeoutCallback(cbTimeout);就是触发真正的发送数据了。

那么数据是怎么发送出去的?上面我们提到cbTimeout回调函数,里面调用了handleTimeout()函数,然后由子类去实现发送逻辑this->sendFrame(frame),在子类中真正的发送逻辑还是在父类中实现的也就是sendRtpPacket(RtpPacket* packet)函数,在这个函数里还执行了一个回调函数,最终发送者是在MediaSession中,在MediaSession中调用了handleSendRtpPacket()函数才将数据最终发送出去。

这是一个基于北小菜RTSP服务器的个人理解,有不对的地方欢迎指正。


http://www.kler.cn/a/288734.html

相关文章:

  • Leecode热题100-35.搜索插入位置
  • WebGIS三维地图框架--Cesium
  • Elastic Observability 8.16:增强的 OpenTelemetry 支持、高级日志分析和简化的入门流程
  • Golang | Leetcode Golang题解之第559题N叉树的最大深度
  • JQuery封装的ajax
  • 算法——移除链表元素(leetcode203)
  • 学习react day01
  • C 语言都有哪些标准版本?
  • @RequestParam对于请求的影响
  • JVM类加载机制与双亲委派模型解析
  • AI大模型之旅-本地安装llm工具dify 和 fastgpt
  • 深度学习100问46:什么是Dropout
  • Unity SceneView 相机聚焦到指定位置
  • C#——XML序列化
  • 利用通义灵码实现我的第一次开源贡献
  • Web服务如何实现一个视频项目架构
  • RCE漏洞
  • Vue 3 中如何对接高德地图
  • goreplay流量重放备忘
  • sqlite3的db.serialize方法:确保数据库操作串行化的利器
  • Autosar OS基础知识导图
  • 根据xml模板导出excel
  • Gitee镜像关联GitHub仓库
  • 【MySQL-24】万字全面解析<索引>——【介绍&语法&性能分析&使用规则】
  • 数据库:实验六存储过程
  • websocket:两台PC间数据传输