百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

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

lipiwang 2025-03-13 17:07 10 浏览 0 评论

一、简介

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;' ifdata12='= 0x00' data13='= 0x00' data14='= 0x01)' nalustartcodeindex='3;' ifdata12='= 0x00' data13='= 0x00' data14='= 0x00' data15='= 0x01)' nalustartcodeindex='4;' int nalutype='data[12' nalustartcodeindex 0x1f logutils.v141--------00 00 01 : nalutype nalutype mark: mark ifnalutype='= 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 data15 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 

#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 data11 logi11-------rtp payloadtype='%d,marker' d timestamp:d size : d header.payloadtype header.marker header.seq header.timestamp size if header.payloadtype='= RTP_PAYLOAD_TYPE_AAC)' dumpaacdata 12 size - 12 header.seq header.timestamp else if header.payloadtype='= RTP_PAYLOAD_TYPE_H264)' dumph264data 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::dumph264const 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 header12bytes nalu header 1byte nalu payload int nalustartcodeindex='0;' if data12='= 0x00' data13='= 0x00' data14='= 0x01)' nalustartcodeindex='3;' if data12='= 0x00' data13='= 0x00' data14='= 0x00' data15='= 0x01)' nalustartcodeindex='4;' int nalutype='data[12' nalustartcodeindex 0x1f logi141--------00 00 01 : nalutype:d mark :d nalustartcodeindex='%d' rtp size='%d' seq='%d",' nalutype marknalustartcodeindexsizeseq 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(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(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 data3 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(adtsFrameBuffer), aacLength + 7,
                                          timestamp);
            }
            delete[] aacRaw;
        } else {
            LOGE("141---音频数据非法,请检查发送端的数据报文");
        }
    } else {
        LOGI("音频数据非法,请检查发送端的数据报文");
    }

}

三、RTP 协议优势

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

相关推荐

linux实例之设置时区的方式有哪些

linux系统下的时间管理是一个复杂但精细的功能,而时区又是时间管理非常重要的一个辅助功能。时区解决了本地时间和UTC时间的差异,从而确保了linux系统下时间戳和时间的准确性和一致性。比如文件的时间...

Linux set命令用法(linux cp命令的用法)

Linux中的set命令用于设置或显示系统环境变量。1.设置环境变量:-setVAR=value:设置环境变量VAR的值为value。-exportVAR:将已设置的环境变量VAR导出,使其...

python环境怎么搭建?小白看完就会!简简单单

很多小伙伴安装了python不会搭建环境,看完这个你就会了Python可应用于多平台包括Linux和MacOSX。你可以通过终端窗口输入"python"命令来查看本地是否...

Linux环境下如何设置多个交叉编译工具链?

常见的Linux操作系统都可以通过包管理器安装交叉编译工具链,比如Ubuntu环境下使用如下命令安装gcc交叉编译器:sudoapt-getinstallgcc-arm-linux-gnueab...

JMeter环境变量配置技巧与注意事项

通过给JMeter配置环境变量,可以快捷的打开JMeter:打开终端。执行jmeter。配置环境变量的方法如下。Mac和Linux系统在~/.bashrc中加如下内容:export...

C/C++|头文件、源文件分开写的源起及作用

1C/C++编译模式通常,在一个C++程序中,只包含两类文件——.cpp文件和.h文件。其中,.cpp文件被称作C++源文件,里面放的都是C++的源代码;而.h文件则被称...

linux中内部变量,环境变量,用户变量的区别

unixshell的变量分类在Shell中有三种变量:内部变量,环境变量,用户变量。内部变量:系统提供,不用定义,不能修改环境变量:系统提供,不用定义,可以修改,可以利用export将用户变量转为环...

在Linux中输入一行命令后究竟发生了什么?

Linux,这个开源的操作系统巨人,以其强大的命令行界面而闻名。无论你是初学者还是经验丰富的系统管理员,理解在Linux终端输入一条命令并按下回车后发生的事情,都是掌握Linux核心的关键。从表面上看...

Nodejs安装、配置与快速入门(node. js安装)

Nodejs是现代JavaScript语言产生革命性变化的一个主要框架,它使得JavaScript从一门浏览器语言成为可以在服务器端运行、开发各种各样应用的通用语言。在不同的平台下,Nodejs的安装...

Ollama使用指南【超全版】(olaplex使用方法图解)

一、Ollama快速入门Ollama是一个用于在本地运行大型语言模型的工具,下面将介绍如何在不同操作系统上安装和使用Ollama。官网:https://ollama.comGithub:http...

linux移植(linux移植lvgl)

1uboot移植l移植linux之前需要先移植一个bootlader代码,主要用于启动linux内核,lLinux系统包括u-boot、内核、根文件系统(rootfs)l引导程序的主要作用将...

Linux日常小技巧参数优化(linux参数调优)

Linux系统参数优化可以让系统更加稳定、高效、安全,提高系统的性能和使用体验。下面列出一些常见的Linux系统参数优化示例,包括修改默认配置、网络等多方面。1.修改默认配置1.1修改默认编辑器默...

Linux系统编程—条件变量(linux 条件变量开销)

条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用。条件变量之所以要和互斥锁一起使用,主要是因为互斥锁的一个明显的特点就是它只有两种状态:锁定和非锁定,而条件变量可以通过允许线程阻塞和等...

面试题-Linux系统优化进阶学习(linux系统的优化)

一.基础必备优化:1.关闭SElinux2.FirewalldCenetOS7Iptables(C6)安全组(阿里云)3.网络管理服务||NetworkManager|network...

嵌入式Linux开发教程:Linux Shell

本章重点介绍Linux的常用操作和命令。在介绍命令之前,先对Linux的Shell进行了简单介绍,然后按照大多数用户的使用习惯,对各种操作和相关命令进行了分类介绍。对相关命令的介绍都力求通俗易懂,都给...

取消回复欢迎 发表评论: