zoukankan      html  css  js  c++  java
  • vlc源码分析(三) 调用live555接收RTP数据

      首先了解RTSP/RTP/RTCP相关概念,尤其是了解RTP协议:RTP与RTCP协议介绍(转载)

      vlc使用模块加载机制调用live555,调用live555的文件是live555.cpp。

    一、几个重要的类  

      以下向左箭头(“<-”)为继承关系。

    1. RTPInterface

      RTPInterface是RTPSource的成员变量,其成员函数handleRead会读取网络数据存入BufferedPacket内,该类最终会调到UDP的发送接收函数。

    Boolean RTPInterface::handleRead(unsigned char* buffer, unsigned bufferMaxSize,
                     unsigned& bytesRead, struct sockaddr_in& fromAddress, Boolean& packetReadWasIncomplete)

    2. BufferedPacket

      BufferedPacket:用于存储媒体的RTP数据包

      BufferedPacket<-H264BufferedPacket:用于存储H264媒体RTP数据包

      该类有一个重要函数fillInData,是由RTPInterface读取数据存入包中。

    Boolean BufferedPacket::fillInData(RTPInterface& rtpInterface, Boolean& packetReadWasIncomplete);

      相对于BufferedPacket,有对应的工厂类:

      BufferedPacketFactory:工厂模式生成BufferedPacket包
      BufferedPacketFactory<-H264BufferedPacketFactory:专门生产H264BufferedPacket的工厂

      在SessionsSetup的时候(也是模块加载的时候),会根据Source类型,选定生产BufferedPacket的工厂类型,即如果Source是H264格式的话,就会new H264BufferedPacketFactory,之后在接收数据的时候就会生产H264BufferedPacket用于存储H264媒体数据。

      ReorderingPacketBuffer:MultiFramedRTPSource的成员变量,用于管理多个BufferedPacket。

    3. Source相关类  

      Source相关类的继承关系:Medium<-MediaSource<-FramedSource<-RTPSource<-MultiFramedRTPSource<-H264VideoRTPSource。
      在SessionsSetup的时候,会根据数据源的类型,选定Source的类型,即如果数据源是H264格式的话,就会调用

    static H264VideoRTPSource* createNew(UsageEnvironment& env, Groupsock* RTPgs,
      unsigned char rtpPayloadFormat,
      unsigned rtpTimestampFrequency = 90000);

    二、播放流程的建立

      播放流程的建立可以参考vlc源码分析之播放流程

    三、接收RTP数据

      vlc在播放IPC时,会开启一个线程接收网络数据,该线程接收网络数据后会调用Demux()进行分离(因为可能是音频,也可能是视频)。Demux()首先将必要的接口,如StreamRead、StreamClose注册下去,然后就进入事件循环:

    p_sys->scheduler->doEventLoop( &p_sys->event_data );

      如果有网络数据到来了,Demux()会做两件事,第一件事是分析RTP包,放入ReorderingPacketBuffer管理的BufferedPacket中,堆栈如下图所示:

      第二件事是读取的BufferedPacket,进行一系列拆包操作后,将数据放入数据fifo中,堆栈如下图所示:

      doEventLoop会进入死循环,直到p_sys->event_data的值被中断或者超时改变,从而退出循环。当有网络数据到来的时候,doEventLoop会执行SingleStep->...->doGetNextFrame1(),在doGetNextFrame1()函数中读取RTP数据。这个过程的代码及注释如下:

    // 做了两件事,一件是分析RTP包,放入ReorderingPacketBuffer管理的BufferedPacket中;
    // 另一件是读取的BufferedPacket,进行一系列拆包操作后,将数据放入数据fifo中
    void MultiFramedRTPSource::networkReadHandler1() {
      BufferedPacket* bPacket = fPacketReadInProgress;
      if (bPacket == NULL) {
        // Normal case: Get a free BufferedPacket descriptor to hold the new network packet:
        bPacket = fReorderingBuffer->getFreePacket(this);
      }
    
      // Read the network packet, and perform sanity checks on the RTP header:
      Boolean readSuccess = False;
      // do-while(0)结构,出现错误直接break
      do {
        Boolean packetReadWasIncomplete = fPacketReadInProgress != NULL;
        if (!bPacket->fillInData(fRTPInterface, packetReadWasIncomplete)) break;
        if (packetReadWasIncomplete) {
          // We need additional read(s) before we can process the incoming packet:
          fPacketReadInProgress = bPacket;
          return;
        } else {
          fPacketReadInProgress = NULL;
        }
    #ifdef TEST_LOSS
        setPacketReorderingThresholdTime(0);
           // don't wait for 'lost' packets to arrive out-of-order later
        if ((our_random()%10) == 0) break; // simulate 10% packet loss
    #endif
    
        // Check for the 12-byte RTP header:
        if (bPacket->dataSize() < 12) break;
        // 读取RTP头,向前移4个字节
        unsigned rtpHdr = ntohl(*(u_int32_t*)(bPacket->data())); ADVANCE(4);
        // 读取RTP头中的标记位
        Boolean rtpMarkerBit = (rtpHdr&0x00800000) != 0;
        // 读取时间戳,向前移4个字节
        unsigned rtpTimestamp = ntohl(*(u_int32_t*)(bPacket->data()));ADVANCE(4);
        // 读取SSRC,向前移4个字节
        unsigned rtpSSRC = ntohl(*(u_int32_t*)(bPacket->data())); ADVANCE(4);
    
        // Check the RTP version number (it should be 2):
        // 检查RTP头版本,不是2的话,break
        if ((rtpHdr&0xC0000000) != 0x80000000) break;
    
        // Skip over any CSRC identifiers in the header:
        // 跳过CSRC计数字节
        unsigned cc = (rtpHdr>>24)&0xF;
        if (bPacket->dataSize() < cc) break;
        ADVANCE(cc*4);
    
        // Check for (& ignore) any RTP header extension
        // 如果扩展头标志被置位
        if (rtpHdr&0x10000000) {
          if (bPacket->dataSize() < 4) break;
          // 获取扩展头
          unsigned extHdr = ntohl(*(u_int32_t*)(bPacket->data())); ADVANCE(4);
          // 获取扩展字节数
          unsigned remExtSize = 4*(extHdr&0xFFFF);
          if (bPacket->dataSize() < remExtSize) break;
          // 直接跳过扩展字节???
          ADVANCE(remExtSize);
        }
    
        // Discard any padding bytes:
        // 如果填充标志被置位,直接丢弃不处理
        if (rtpHdr&0x20000000) {
          if (bPacket->dataSize() == 0) break;
          unsigned numPaddingBytes
        = (unsigned)(bPacket->data())[bPacket->dataSize()-1];
          if (bPacket->dataSize() < numPaddingBytes) break;
          bPacket->removePadding(numPaddingBytes);
        }
        // Check the Payload Type.
        // 检查载荷类型,如果源数据H264类型,则其值为96
        // 如果与我们生成的source类型不同,则break
        if ((unsigned char)((rtpHdr&0x007F0000)>>16)
        != rtpPayloadFormat()) {
          break;
        }
    
        // The rest of the packet is the usable data.  Record and save it:
        if (rtpSSRC != fLastReceivedSSRC) {
          // The SSRC of incoming packets has changed.  Unfortunately we don't yet handle streams that contain multiple SSRCs,
          // but we can handle a single-SSRC stream where the SSRC changes occasionally:
          fLastReceivedSSRC = rtpSSRC;
          fReorderingBuffer->resetHaveSeenFirstPacket();
        }
        // RTP包序号,随RTP数据包而自增,由接收者用来探测包损失
        unsigned short rtpSeqNo = (unsigned short)(rtpHdr&0xFFFF);
        Boolean usableInJitterCalculation
          = packetIsUsableInJitterCalculation((bPacket->data()),
                              bPacket->dataSize());
        struct timeval presentationTime; // computed by:
        Boolean hasBeenSyncedUsingRTCP; // computed by:
        // 根据数据包的一些信息,进行一些计算和记录
        receptionStatsDB()
          .noteIncomingPacket(rtpSSRC, rtpSeqNo, rtpTimestamp,
                  timestampFrequency(),
                  usableInJitterCalculation, presentationTime,
                  hasBeenSyncedUsingRTCP, bPacket->dataSize());
    
        // Fill in the rest of the packet descriptor, and store it:
        struct timeval timeNow;
        gettimeofday(&timeNow, NULL);
        // 将计算所得的一些参数再赋值到包中
        bPacket->assignMiscParams(rtpSeqNo, rtpTimestamp, presentationTime,
                      hasBeenSyncedUsingRTCP, rtpMarkerBit,
                      timeNow);
        // 经过以上判断和检查,没有发现问题,则由管理类fReorderingBuffer存储包
        if (!fReorderingBuffer->storePacket(bPacket)) break;
    
        readSuccess = True;// 读取成功
      } while (0);
      if (!readSuccess) fReorderingBuffer->freePacket(bPacket);// 如果读取不成功,则释放内存
    
      // 将读取到的数据包送至数据fifo中,等待解码线程解码
      doGetNextFrame1();
      // If we didn't get proper data this time, we'll get another chance
    }

    四、组装RTP包为音视频数据包

    4.1 组帧要点

    1. 默认一个音频包就是一个音频帧;

    2. sps,pps会和I帧组成一个完整的帧;

    3. 帧头:对于sps,默认为帧头。对于判断是slice开头的包,根据片中第一个宏块的地址(first_mb_in_slice,读取时注意是哥伦布编码的)判断当前片是否是视频帧的首片;注意,h264组帧协议RFC3984里,FU-header中的S和E标识的是片头和片尾,不是帧。

    4. 帧尾:对于视频数据,如果marker为1,则该片对应的帧为帧尾。

    5. 同一视频帧有相同的timestamp;

    4.2 组帧实例

     以h264组帧帧为例,组帧协议是RFC3984。上层读取到一个个的RTP包之后,需要读取RTP包的payload组装成h264的视频帧。那么按照什么规则组装呢?这里有一个rfc3984协议,描述了RTP封装h264的方法。简单描述如下:

    1. 单个NAL单元包:荷载中只包含一个NAL单元。NAL头类型域等于原始 NAL单元类型,即在范围1到23之间,此时的RTP包仅仅是将NALU的数据前加12个字节的RTP头,即RTP header(12 bytes)+NALU;

    2. 聚合包:本类型用于聚合多个NAL单元到单个RTP荷载中。本包有四种版本,单时间聚合包类型A (STAP-A),单时间聚合包类型B (STAP-B),多时间聚合包类型(MTAP)16位位移(MTAP16), 多时间聚合包类型(MTAP)24位位移(MTAP24)。赋予STAP-A, STAP-B, MTAP16, MTAP24的NAL单元类型号分别是 24,25, 26, 27;

    3. 分片单元:用于分片单个NAL单元到多个RTP包。现存两个版本FU-A,FU-B,用NAL单元类型 28,29标识;

        NAL单元的一个分片由整数个连续NAL单元字节组成。每个NAL单元字节必须正好是该NAL单元一个分片的一部分。相同NAL单元的分片必须使用递增的RTP序号连续顺序发送(第一和最后分片之间没有其他的RTP包)。相似,NAL单元必须按照RTP顺序号的顺序装配。

    格式如下:RTP header(12 bytes)+FU indicator(1 bytes)+FU header(1 bytes)+fu payload

    FU indicator格式如下:

    +---------------+

    |0|1|2|3|4|5|6|7|

    |F|NRI| Type |

    +---------------+

    F、NRI是原始NALU的前3位,Type指示的是FU的type

    FU header格式如下:

    +---------------+

    |0|1|2|3|4|5|6|7|

    |S|E|R| Type |

    +---------------+

    S: 1 bit 当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。

    E: 1 bit 当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的 FU荷载不是分片NAL单元的最后分片,结束位设置为0。

    R: 1 bit 保留位必须设置为0,接收者必须忽略该位

    Type指示的是NALU的type。

    注意:原始的NAL头的前三位为FU indicator的前三位,原始的NAL头的后五位为FU header的后五位。解包时,取FU indicator的前三位和FU Header的后五位,即为NALU的首字节。

    下图为笔者抓到的一个RTP包,前12个字节是RTP header。5c开始是RTP数据,5c是FU indicator,41是FU header,换算为二进制:

    0   是F

    10 是NRI

    11100 是FU type,这里是28

    0 是s

    1 是E,说明是1帧的结束

    0 保留,必须置0

    00001 是NALU type,表示非IDR帧

         将读取到的音视频数据包送至数据fifo中,之后就是等待解码线程从数据fifo中拿数据,解码和渲染了,具体可参考vlc源码分析之播放流程

      附:

      配置好的Windows版vlc工程下载:https://github.com/jiayayao/vlc_2.1.0-vs_2010,下载后使用vs2010可以直接编译运行,调试学习非常方便。

  • 相关阅读:
    IE8、IE9解决浏览器跨域。
    英语写作-Introduction
    qt添加图标
    Qt 编译错误 :cannot find file .pro
    python
    数据集
    基金
    visio2010求交操作
    书籍网站
    ROS安装xtion
  • 原文地址:https://www.cnblogs.com/jiayayao/p/6819722.html
Copyright © 2011-2022 走看看