一文读懂流媒体协议之RTP 协议

一、简介

1.1 RTP

RTP全名是Real-time Transport Protocol(实时传输协议)。它是IETF提出的一个标准,对应的RFC文档为RFC3550(RFC1889为其过期版本)。RFC3550不仅定义了RTP,而且定义了配套的相关协议RTCP(Real-time Transport Control Protocol,即实时传输控制协议)。RTP用来为IP网上的语音、图像、传真等多种需要实时传输的多媒体数据提供端到端的实时传输服务。RTP为Internet上端到端的实时传输提供时间信息和流同步,但并不保证服务质量,服务质量由RTCP来提供。

1.2 RTP 使用场景

RTP协议详细说明了在互联网上传递音频和视频的标准数据包格式,通常用于需要传输音视频流的场景中。诸如在线课堂,在线会议,音视频通话。RTP 本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于低层服务去实现这一过程。RTP 并不保证传送或防止无序传送,也不确定底层网络的可靠性。RTP 实行有序传送, RTP 中的序列号允许接收方重组发送方的包序列,同时序列号也能用于决定适当的包位置。

二、RTP 详解

2.1 在网络体系中的层次

从开发者角度来讲RTP 是位于TCP/UDP 之上的应用层协议,因为RTP的实现还是要靠开发者自己,因而可以看成应用层协议也不为不妥。RTP实现者在发送RTP数据时,需先将数据封装成RTP包,而在接收到RTP数据包,需要将数据从RTP包中提取出来。

2.2 RTP 会话过程

当应用程序建立一个RTP会话时,应用程序将确定一对目的传输地址。目的传输地址由一个网络地址和一对端口组成,有两个端口:一个给RTP包,一个给RTCP包,使得RTP/RTCP数据能够正确发送。RTP数据发向偶数的UDP端口,而对应的控制信号RTCP数据发向相邻的奇数UDP端口(偶数的UDP端口+1),这样就构成一个UDP端口对。 RTP的发送过程如下,接收过程则相反。

  • RTP协议从上层接收流媒体信息码流(如H.264),封装成RTP数据包;RTCP从上层接收控制信息,封装成RTCP控制包。
  • RTP将RTP 数据包发往UDP端口对中偶数端口;RTCP将RTCP控制包发往UDP端口对中的接收端口

2.3 RTP 组包与收包

2.3.1 RTP 协议头

任何一个协议都包含发送端和解析端,发送端按照协议格式进行数据组包发送,接收端按照协议格式接收再解包。协议如何组包首先得先知道其协议格式,每个字节代表的含义,然后按照格式填充数据发送即可。接收端按照协议再进行数据的拆包,拿到想要的数据即完成一次数据的传输。RTP 固定头格式如下:

  • 版本号(V):2比特,用来标志使用的RTP版本。
  • 填充位(P):1比特,如果该位置位,则该RTP包的尾部就包含附加的填充字节。
  • 扩展位(X):1比特,如果该位置位的话,RTP固定头部后面就跟有一个扩展头部。
  • CSRC计数器(CC):4比特,含有固定头部后面跟着的CSRC的数目。
  • 标记位(M):1比特,该位的解释由配置文档(Profile)来承担.
  • 不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。 由于音频帧比较小,一个RTP包就是一个音频帧,所以该位直接置1
  • 视频,有些帧特别大已经超过了TCP/UDP 的MTU,所以需要对当前帧进行分片。如下几种情况需要置为1,意味着本帧结束
  • sps pps 帧 为1
  • 单帧包 为1
  • 分片包/组合包 最后一帧为1,其他为0
  • 载荷类型(PT):7比特,标识了RTP载荷的类型,通常在这里区分是负载音频还是视频数据。
  • 序列号(SN):16比特,发送方在每发送完一个RTP包后就将该域的值增加1,接收方可以由该域检测包的丢失及恢复包序列。序列号的初始值是随机的。
  • 时间戳:32比特,记录了该包中数据的第一个字节的采样时刻。在一次会话开始时,时间戳初始化成一个初始值。即使在没有信号发送时,时间戳的数值也要随时间而不断地增加(时间在流逝嘛)。时间戳是去除抖动和实现同步不可缺少的。
  • 同步源标识符(SSRC):32比特,同步源就是指RTP包流的来源。在同一个RTP会话中不能有两个相同的SSRC值。该标识符是随机选取的 RFC1889推荐了MD5随机算法。
  • 贡献源列表(CSRC List):0~15项,每项32比特,用来标志对一个RTP混合器产生的新包有贡献的所有RTP包的源。由混合器将这些有贡献的SSRC标识符插入表中。SSRC标识符都被列出来,以便接收端能正确指出交谈双方的身份。

2.3.2 组包的核心代码如下

/**
   * 通过 rtp 发送 数据包,包组织结构
   * 单个视频包: rtp head + 去掉h264 起始码的NALU
   * 分片:[rtp head] [FU indicator] [FU head]
   * @param prefixData [FU indicator] [FU head] 各1 个字节,如果有,则填上,没有可不填
   * @param data   nalu 数据
   * @param offset
   * @param size
   * @param mark  M 标志,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
   *              由于音频帧比较小,一个RTP包就是一个音频帧,所以该位直接置1
   *              视频,如下几种情况需要置为1,意味着本帧结束
   *              1.sps pps 帧 为1
   *              2.单帧包 为1
   *              3.分片包/组合包  最后一帧为1,其他为0
   *
   * @param seqNum 序列号
   * @param timeUs 时间戳
   * @throws IOException
   */
  public void addPacket(byte[] prefixData, byte[] data, int offset, int size, int mark,short seqNum,long timeUs) throws IOException {

/*
RTP packet header
Bit offset[b]  0-1    2  3  4-7    8  9-15   16-31
0        Version    P  X  CC M  PT Sequence Number  31
32       Timestamp                           63
64       SSRC identifier                          95
---------------------------------------------------------------------------------------
|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
---------------------------------------------------------------------------------------
|v  |p|x|cc-----|m|-------pt7bit------|------------------seq 16bit--------------------|
---------------------------------------------------------------------------------------
      |-----------------------time stamp 32 bit---------------------------------------------|
      ---------------------------------------------------------------------------------------
      |------------------------------ssrc 32bit---------------------------------------------|
*/

      ByteBuffer buffer = ByteBuffer.allocate(500000);
      //存放v p x cc  1 字节
      buffer.put((byte)(2 << 6));

      //存放 m、 pt  1 个字节 96 = 0x 0110 0000
      // m = 1 pt = 100 0000
      // mark << 7 = x000 0000 得到mark 标志值
      // 再与 payloadType | 操作  得到一个字节的  m_payloadType
      buffer.put((byte)((mark<< 7) | payloadType));

      //序列号 16 位 2 字节
      buffer.putShort(seqNum);
      //时间戳 4字节
      buffer.putInt((int)(timeUs));
      //同步信源 ssrc 4字节SSRC相当于一个RTP传输session的ID,就象每个人都有一个名字一样,
      //每一个RTP传输也都有一个名字。这个数字是随机产生,并且要保证唯一。当RTP session改变(如IP等)时,这个ID也要改变
      buffer.putInt(getSsrc());

      //提供csrc 标识符 由前面的cc 指定
      //buffer.putInt(size);

if (prefixData != null) {
    buffer.put(prefixData);
}

      buffer.put(data, offset, size);

      sendPacket(buffer, buffer.position());

  }

2.3.3 收包核心代码

private boolean dumpRtp(byte[] data, int size) {
    if (data != null) {

        //分析12字节的头部数据
        //读取1 字节 得到 v p x cc
        int v = (data[0] & 0xc0) >> 6;
        int p = (data[0] & 0x20) >> 5;
        int x = (data[0] & 0x10) >> 4;
        int cc = data[0] & 0x0f;

        //读取第2字节的第1位 得到 mark 标志
        int mark = (data[1] & 0x80) >> 7;

        //读取 第2 字节的后7位得到payloadType 用来区分 流类型
        int payLoadType = data[1] & 0x7f;

        //读取2字节 得到序列号
        int sequenceNumber = (data[2] << 8 & 0xff00) | (data[3] & 0x00ff);

        //读取 4 byte 得到序列号
        int timeStamp = (data[4] << 24 & 0xff000000) | (data[5] << 16 & 0x00ff0000) | (data[6] << 8 & 0x0000ff00)
            | (data[7] & 0x000000ff);

        //同步信源 ssrc 4字节SSRC
        int ssrc = (data[8] << 24) | (data[9] << 16) | (data[10] << 8) | data[11];

        //LogUtils.v("106--------:mark = " + mark + "  payloadType :" +
        //    payLoadType + " seqNum:" + sequenceNumber + " timeStamp:" + timeStamp + " size: " + size);

        if (payLoadType == VIDEO_TYPE) {
            if(size <= MTU) {
                int naluStartCodeIndex = 0;
                if(data[12] == 0x00 && data[13] == 0x00 && data[14] == 0x01) {
                    naluStartCodeIndex = 3;
                }
                if(data[12] == 0x00 && data[13] == 0x00 && data[14] == 0x00 && data[15] == 0x01) {
                    naluStartCodeIndex = 4;
                }

                int naluType = data[12 + naluStartCodeIndex] & 0x1f;
                LogUtils.v("141--------00 00 01 单一帧类型: naluType "+naluType +" mark:" + mark);

                //分片包的最后一组数据
                if(naluType == FRAGMENT_UNIT_A_TYPE) {
                    int s = (data[13] & 0x80) >> 7;
                    int e = (data[13] & 0x40) >> 6;
                    int frameType = data[13] & 0x1f;
                    LogUtils.v("141-----------分片包的最后一组数据: s = " + s +" e:" + e + " frameType :" + frameType);
                    //分片结束,可以判断 e = 1,其实
                    if(e == 1) {
                        System.arraycopy(data, 12 + 2 + naluStartCodeIndex, h264Buffer, frameSize, size - 12 - 2 - naluStartCodeIndex);
                        frameSize += (size - 12 - 2 - naluStartCodeIndex);
                        //拷贝一帧完整的h264 码流数据回调出去
                        byte[] frameBuffer = new byte[frameSize];
                        System.arraycopy(h264Buffer,0,frameBuffer,0,frameSize);
                        frameSize = 0;
                        hasAddNaluHead = false;
                        if (onDumpListener != null) {
                            onDumpListener.onVideo(frameBuffer, frameType, sequenceNumber, timeStamp);
                        }
                    } else {
                        LogUtils.e("分片包的最后一帧 e 标志位异常 e= :" + e);
                    }


                } else {
                    byte[] h264 = new byte[size - 12 - naluStartCodeIndex];
                    System.arraycopy(data, 12 + naluStartCodeIndex, h264, 0, size - 12 - naluStartCodeIndex);
                    if (onDumpListener != null) {
                        onDumpListener.onVideo(h264, naluType, sequenceNumber, timeStamp);
                    }
                }


            } else {
                int naluStartCodeIndex = 0;
                if(data[14] == 0x00 && data[15] == 0x00 && data[16] == 0x01) {
                    naluStartCodeIndex = 3;
                }
                if(data[14] == 0x00 && data[15] == 0x00 && data[16] == 0x00 && data[17] == 0x01) {
                    naluStartCodeIndex = 4;
                }
                //进入分片模式 [RTP head 12byte] [FU indicator 1byte] [FU head 1byte] [data]
                /**
                 * 读取1字节
                 *   FU-A indicator type
                 *   +---------------+
                 *   |0|1|2|3|4|5|6|7|
                 *   +-+-+-+-+-+-+-+-+
                 *   |F|NRI|  Type   |
                 *   +---------------+
                 *   h264 码流时:
                 *   F (1bit) ---H264 F 全称forbidden_zero_bit,禁止位
                 *   作用:当网路发现nal单元有比特错误时,可以设置为1,以便接收方丢掉改单元否则默认为0
                 *   NRI 2bit) 对应 h264 NRI 全称nal_ref_idc,重要性指示位,
                 *   作用:标志改NAL单元用于重建时的重要性,
                 *   值越大越重要,取值范围00~11
                 *   type:FU-A  为 28
                 */
                //FU-A indicator 的后5位为 分片类型,此处只处理FU-A = 28 的类型
                int fuAType = data[12] & 0x1f;
                if(fuAType == FRAGMENT_UNIT_A_TYPE) {
                    /**
                     *   +---------------+
                     *   |0|1|2|3|4|5|6|7|
                     *   +-+-+-+-+-+-+-+-+
                     *   |S|E|R|  Type   |
                     *   +---------------+
                     *
                     *   H264 各字段含义:
                     *   S,为1表示分片的开始;
                     *   E,为1表示分片的结束,否则为0;
                     *   R,保留位;
                     *   Type就是NALU头中的Type,取1-23值。
                     */
                    int s = (data[13] & 0x80) >> 7;
                    int e = (data[13] & 0x40) >> 6;
                    int naluType = data[13] & 0x1f;

                    //nalu head = FU-A indicator 前3位 的F NRI + FU head 后5位 Type
                    byte naluHead = (byte)((data[12] & 0xe0 ) | (data[13] & 0x1f));
                    LogUtils.v("175---------s :" + s +"  e:" + e +" nalueType:" + naluType +" size:" + size + " fu_type :" + ByteUtil.bytesToHexString(new byte[]{data[13]}));
                    //分片开始
                    if(e == 0) {

                        //再拷贝数据,剔除起始码数据
                        System.arraycopy(data, 12 + 2 + naluStartCodeIndex, h264Buffer, frameSize, size - 12 - 2 - naluStartCodeIndex);
                        frameSize += (size - 12 - 2 - naluStartCodeIndex);

                    } else {
                        LogUtils.e("分片包的开始包 e 标志位异常 e= :" + e);
                    }
                }
            }


        } else if (payLoadType == AUDIO_TYPE) {
            // 其中RTP载荷的一个字节为0x00,第二个字节为0x10。
            // 第三个字节和第四个字节保存AAC Data的大小,最多只能保存13bit,
            // 第三个字节保存数据大小的高八位,
            // 第四个字节的高5位保存数据大小的低5位。
            // 注意 取出的 aac 为不带adts 头的数据
            int aacLength = ((data[14] & 0xFF) << 5) + ((data[15] & 0xF8) >> 3);
            LogUtils.v("收到音频帧长度为: "+aacLength +" size :" + size);
            if(aacLength == (size - 16)) {
                byte[] aacRaw = new byte[aacLength];
                System.arraycopy(data,16,aacRaw,0,aacLength);
                //此处也可以不添加adts 头,当不添加adts 头时,需要配置mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 0);
                byte[] aacFrame = addAdtsHeader(aacRaw);
                if(onDumpListener != null) {
                    onDumpListener.onAudio(aacFrame,sequenceNumber,timeStamp);
                }
            } else {
                LogUtils.v("音频数据非法,请检查发送端的数据报文");
            }



        }



        return true;

    } else {
        LogUtils.e("待解析的rtp 数据异常,停止解析");
        return false;
    }
}

2.2.4 C++ 版本组包:

#include <stdint.h>

#define RTP_VESION              2

#define RTP_PAYLOAD_TYPE_H264   96
#define RTP_PAYLOAD_TYPE_AAC    97

#define RTP_HEADER_SIZE         12
#define RTP_MAX_PKT_SIZE        1400
#define FRAGMENT_UNIT_A_TYPE    28

/*
 *
 *    0                   1                   2                   3
 *    7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |V=2|P|X|  CC   |M|     PT      |       sequence number         |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |                           timestamp                           |
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   |           synchronization source (SSRC) identifier            |
 *   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
 *   |            contributing source (CSRC) identifiers             |
 *   :                             ....                              :
 *   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *   其中的:n是一种位表示法,这个结构体跟RTP的头部一一对应
 */

struct RtpHeader
{
    /* byte 0 */
    uint8_t csrcLen:4;
    uint8_t extension:1;
    uint8_t padding:1;
    uint8_t version:2;

    /* byte 1 */
    uint8_t payloadType:7;
    uint8_t marker:1;

    /* bytes 2,3 */
    uint16_t seq;

    /* bytes 4-7 */
    uint32_t timestamp;

    /* bytes 8-11 */
    uint32_t ssrc;
};

struct RtpPacket
{
    struct RtpHeader rtpHeader;
    uint8_t payload[0];
};

class rtp {

public:
    rtp();
    ~rtp();

    void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,
                       uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,
                       uint16_t seq, uint32_t timestamp, uint32_t ssrc);

    int rtpSendPacket(int socket, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize);

};


源文件:

组包源文件:

rtp::rtp() {

}

rtp::~rtp() {

}

void rtp::rtpHeaderInit(struct RtpPacket *rtpPacket, uint8_t csrcLen, uint8_t extension, uint8_t padding, uint8_t version,
                   uint8_t payloadType, uint8_t marker, uint16_t seq, uint32_t timestamp, uint32_t ssrc) {

    rtpPacket->rtpHeader.csrcLen = csrcLen;
    rtpPacket->rtpHeader.extension = extension;
    rtpPacket->rtpHeader.padding = padding;
    rtpPacket->rtpHeader.version = version;
    rtpPacket->rtpHeader.payloadType =  payloadType;
    rtpPacket->rtpHeader.marker = marker;
    rtpPacket->rtpHeader.seq = seq;
    rtpPacket->rtpHeader.timestamp = timestamp;
    rtpPacket->rtpHeader.ssrc = ssrc;

}

int rtp::rtpSendPacket(int socket, char *ip, int16_t port, struct RtpPacket *rtpPacket, uint32_t dataSize) {
    struct sockaddr_in addr;
    int ret;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);

    rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);
    rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);
    rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);

    ret = sendto(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE, 0,
                 (struct sockaddr*)&addr, sizeof(addr));

    rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);
    rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);
    rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);

    return ret;
}

收包:

void dumpHead(const char *data, int32_t size);

/**
 * 从数据data 中解析h264裸流数据
 * @param data       rtp 数据报文
 * @param size       rtp 数据报文大小
 * @param seq        rtp 数据报文序列号,非常重要判断丢帧的重要标志
 * @param timestamp  rtp 数据报文时间戳 作同步用
 * @param mark       rtp 数据报文 头部mark字段1 表示一帧结束,0表示未结束,通常用于分片中
 */
void dumpH264(const char *data, int32_t size, uint16_t seq, uint32_t timestamp,uint32_t mark);

/**
 * 从数据data 中解析aac裸流数据
 * @param data       rtp 数据报文
 * @param size       rtp 数据报文大小
 * @param seq        rtp 数据报文序列号,非常重要判断丢帧的重要标志
 * @param timestamp  rtp 数据报文时间戳 作同步用
 */
void dumpAAC(const char *data, int32_t size, uint16_t seq, uint32_t timestamp);
void RtpDemux::dumpHead(const char *data, int32_t size) {
    if (data && size > 0) {
        RtpHeader header = {0};
        //第1字节
        header.version = data[0] & 0xc0;
        header.padding = data[0] & 0x20;
        header.extension = data[0] & 0x10;
        header.csrcLen = data[0] & 0x0f;

        //第2字节 m_pt
        header.marker = (data[1] & 0x80) >> 7;
        header.payloadType = data[1] & 0x7f;
        //2-3字节  序列号
        header.seq = (data[2] << 8 & 0xff00) | (data[3] & 0x00ff);
        //4-7字节序列号
        header.timestamp = (data[4] << 24 & 0xff000000) | (data[5] << 16 & 0x00ff0000) | (data[6] << 8 & 0x0000ff00)
                           | (data[7] & 0x000000ff);
        //8-11字节同步信号源
        header.ssrc = (data[8] << 24) | (data[9] << 16) | (data[10] << 8) | data[11];
        LOGI("11-------收到RTP数据包 payloadType= %d,marker = %d, 序列号:%d, timeStamp:%d size : %d", header.payloadType,
             header.marker, header.seq, header.timestamp, size);
        if (header.payloadType == RTP_PAYLOAD_TYPE_AAC) {
            dumpAAC(data + 12, size - 12, header.seq, header.timestamp);
        } else if (header.payloadType == RTP_PAYLOAD_TYPE_H264) {
            dumpH264(data, size, header.seq, header.timestamp, header.marker);
        }

    } else {
        LOGE("参数非法,请检查传进来的数据报文");
    }
}

/**
 * 解析rtp 中的 h264 数据
 * @param data  未去除12 字节  rtp 头的h264 数据
 * @param size
 * @param seq
 * @param timestamp
 * @param mark
 */
void RtpDemux::dumpH264(const char *data, int32_t size, uint16_t seq, uint32_t timestamp, uint32_t mark) {
    if (data && size > 0) {
        if (size <= RTP_MAX_PKT_SIZE + RTP_HEADER_SIZE) {
            //RTP header(12bytes) + NALU header (1byte) + NALU payload
            int naluStartCodeIndex = 0;
            if (data[12] == 0x00 && data[13] == 0x00 && data[14] == 0x01) {
                naluStartCodeIndex = 3;
            }
            if (data[12] == 0x00 && data[13] == 0x00 && data[14] == 0x00 && data[15] == 0x01) {
                naluStartCodeIndex = 4;
            }

            int naluType = data[12 + naluStartCodeIndex] & 0x1f;
            LOGI("141--------00 00 01 单一帧类型: naluType:%d  mark :%d naluStartCodeIndex = %d rtp size = %d  seq = %d",
                    naluType, mark,naluStartCodeIndex,size,seq);
            if (naluType == FRAGMENT_UNIT_A_TYPE) {
                int s = (data[13] & 0x80) >> 7;
                int e = (data[13] & 0x40) >> 6;
                int frameType = data[13] & 0x1f;
                LOGE("141-----------分片包的最后一组数据: s = %d  e = %d , mark = %d, "
                     "frameType = %d  data[13] = %x  rtp size = %d seq = %d  fu-indicator = %x  fu-head = %x",
                     s, e, mark,
                     frameType, data[13],size,seq,data[12],data[13]);
                //分片结束,可以判断 e = 1,其实 也可以判断 mark == 1 && e == 1 这样更严谨一点
                if (e == 1) {
                    memcpy(m_h264Buf + m_nHasUnitFrameSize, data + 12 + 2 + naluStartCodeIndex,
                           size - 12 - 2 - naluStartCodeIndex);
                    m_nHasUnitFrameSize += (size - 12 - 2 - naluStartCodeIndex);

                    //拷贝一帧完整的h264 码流数据回调出去
                    char *frameBuffer = new char[m_nHasUnitFrameSize];
                    memset(frameBuffer, 0, m_nHasUnitFrameSize);
                    memcpy(frameBuffer, m_h264Buf, m_nHasUnitFrameSize);
                    //int frameSize = m_nHasUnitFrameSize;
                    if (this->m_callJava) {
                        this->m_callJava->onVideo(THREAD_CHILD, frameType,
                                                  reinterpret_cast<unsigned char *>(frameBuffer),
                                                  m_nHasUnitFrameSize, timestamp, frameType == H264_IDR);
                    }

                    m_nHasUnitFrameSize = 0;
                    memset(m_h264Buf, 0, H264_MTU);
                    delete[] frameBuffer;

                } else {
                    LOGE("141-----分片包的最后一帧 e 标志位异常 e= :%d", e);
                }

            } else {

                char *h264 = new char[size - 12 - naluStartCodeIndex];
                memset(h264, 0, size - 12 - naluStartCodeIndex);
                memcpy(h264, data + 12 + naluStartCodeIndex, size - 12 - naluStartCodeIndex);
                //回调的数据不带起始码
                if (this->m_callJava) {
                    this->m_callJava->onVideo(THREAD_CHILD, naluType, reinterpret_cast<unsigned char *>(h264),
                                              size - 12 - naluStartCodeIndex, timestamp, naluType == H264_IDR);
                }

                delete [] h264;

            }

        } else {
            //进入分片模式 [RTP head 12byte] [FU indicator 1byte] [FU head 1byte] [data]
            int naluStartCodeIndex = 0;
            if (data[14] == 0x00 && data[15] == 0x00 && data[16] == 0x01) {
                naluStartCodeIndex = 3;
            }
            if (data[14] == 0x00 && data[15] == 0x00 && data[16] == 0x00 && data[17] == 0x01) {
                naluStartCodeIndex = 4;
            }
            /**
             * 读取1字节
             *   FU-A indicator type
             *   +---------------+
             *   |0|1|2|3|4|5|6|7|
             *   +-+-+-+-+-+-+-+-+
             *   |F|NRI|  Type   |
             *   +---------------+
             *   h264 码流时:
             *   F (1bit) ---H264 F 全称forbidden_zero_bit,禁止位
             *   作用:当网路发现nal单元有比特错误时,可以设置为1,以便接收方丢掉改单元否则默认为0
             *   NRI 2bit) 对应 h264 NRI 全称nal_ref_idc,重要性指示位,
             *   作用:标志改NAL单元用于重建时的重要性,
             *   值越大越重要,取值范围00~11
             *   type:FU-A  为 28
             */
            //FU-A indicator 的后5位为 分片类型,此处只处理FU-A = 28 的类型
            int fuAType = data[12] & 0x1f;
            if (fuAType == FRAGMENT_UNIT_A_TYPE) {
                /**
                 *   +---------------+
                 *   |0|1|2|3|4|5|6|7|
                 *   +-+-+-+-+-+-+-+-+
                 *   |S|E|R|  Type   |
                 *   +---------------+
                 *
                 *   H264 各字段含义:
                 *   S,为1表示分片的开始;
                 *   E,为1表示分片的结束,否则为0;
                 *   R,保留位;
                 *   Type就是NALU头中的Type,取1-23值。
                 */
                int s = (data[13] & 0x80) >> 7;
                int e = (data[13] & 0x40) >> 6;
                int naluType = data[13] & 0x1f;

                //nalu head = FU-A indicator 前3位 的F NRI + FU head 后5位 Type
                char naluHead = ((data[12] & 0xe0) | (data[13] & 0x1f));
                LOGI("175---------收到分片包 s : %d e : %d  seq = %d, naluType : %d size : %d", s, e, seq,naluType, size);
                //分片开始
                if (e == 0) {
                    //剔除起始码数据,再拷贝数据
                    memcpy(m_h264Buf + m_nHasUnitFrameSize, data + 12 + 2 + naluStartCodeIndex,
                           size - 12 - 2 - naluStartCodeIndex);
                    m_nHasUnitFrameSize += size - 12 - 2 - naluStartCodeIndex;
                } else {
                    LOGE("分片包的开始包 e 标志位异常 e= :%d", e);
                }
            }
        }
    }
}

void RtpDemux::dumpAAC(const char *data, int32_t size, uint16_t seq, uint32_t timestamp) {
    if (data && size > 0) {
        int aacLength = ((data[2] & 0xFF) << 5) + ((data[3] & 0xF8) >> 3);
        //LOGI("133----收到音频包数据大小为:%d", aacLength);
        if (aacLength == (size - 4)) {
            char *aacRaw = new char[aacLength];
            memset(aacRaw, 0, aacLength);
            memcpy(aacRaw, data + 4, aacLength);

            char *adtsFrameBuffer = addADTSAAC(aacRaw, aacLength);

            if (this->m_callJava) {
                this->m_callJava->onAudio(THREAD_CHILD, AUDIO_AAC_ADTS_FRAME,
                                          reinterpret_cast<uint8_t *>(adtsFrameBuffer), aacLength + 7,
                                          timestamp);
            }
            delete[] aacRaw;
        } else {
            LOGE("141---音频数据非法,请检查发送端的数据报文");
        }
    } else {
        LOGI("音频数据非法,请检查发送端的数据报文");
    }

}

三、RTP 协议优势

  1. 对于UDP 传输,提供了包乱序解决方案
  2. 解决方案:可以根据RTP包的序列号来排序。
  3. 提供了时间戳与包序列号两种机制来确定发包顺序,当出现包丢失时可根据丢失的包进行重发而不必重发整个报文。
  4. 提供解决音视频同步的方案
  5. 根据声音流和图像流的相对时间(即RTP包的时间戳),以及它们的绝对时间(即对应的RTCP包中的RTCP),可以实现声音和图像的同步
原文链接:,转发请注明来源!