jjzjj

流媒体基础-RTP协议

毕加索解锁 2023-05-25 原文

RTP 提供带有 实时特性的端对端数据传输服务,传输的数据如:交互式的音频和视频。那些服务包括有效载荷类型定义、序列号、时间戳和传输监测控制。RTP支持数据使用多播分发机制转发到多个目的地

注意 RTP 本身没有提供任何的机制来确保实时的传输或其他的服务质量保证,而是由低层的服务来完成。它不保证传输或防止乱序传输,它不假定下层网络是否可靠,是否按顺序传送数据包。RTP 包含的序列号允许接受方重构发送方的数据包顺序,但序列号也用来确定一个数据包的正确位置。

1、RTP的应用环境

RTP用于在单播或多播网络中传送实时数据。它们典型的应用场合如下:

简单的多播音频会议

  • 语音通信通过一个多播地址和一对端口来实现;
  • 一个端口用于音频数据(RTP)
  • 一个端口用于RTP控制包(RTCP)

音频和视频会议

  • 如果在一次会议中同时使用了音频和视频会议,这两种媒体将分别在不同的RTP会话中传送,每个会话使用不同的传输地址(IP地址+端口)
  • 如果一个用户同时使用了两个会话,则每个会话对应的RTCP包都使用规范化名字CNAME(Canonical Name)
  • 与会者可以根据RTCP包中的CNAME来获取相关联的音频和视频,然后根据RTCP包中的计时信息(Network time protocal)来实现音频和视频的同步。

翻译器和混合器

  • 翻译器和混合器都是RTP级的中继系统,翻译器用在通过IP多播不能直接到达用户区,例如发送者和接收者之间存在防火墙
  • 当与会者能接收的音频编码格式不一样,比如有一个与会者通过一条低速链路接入高速会议,这是就要使用混合器。在进入音频数据格式需要变化的网络前,混合器将来自一个源或多个源的音频包进行重构,并把重构后的多个音频合并,采用另一种音频编码进行编码后,再转发这个新的RTP包。
  • 从一个混合器出来的所有数据包要用混合器作为它们的同步源(SSRC,见RTP封装)来识别,可以通过贡献源列表(CSRC表,见RTP的封装)可确认谈话者。

2、流媒体

流媒体是指Internet上使用流式传输技术的连续时基媒体。当前在Internet上传输音频和视频等信息主要有两种方式:

文件下载

下载情况下,用户需要先下载整个媒体文件到本地,然后才能播放媒体文件。在视频直播等应用场合,由于生成整个媒体文件要等直播结束,也就是用户至少要在直播结束后才能看到直播节目,所以用下载方式不能实现直播。

流式传输

流式传输是实现流媒体的关键技术,使用流式传输可以边下载边观看流媒体节目。

由于Internet是基于分组传输的,所以接收端收到的数据包往往有延迟和乱序(流式传输构建在UDP上)。要实现流式传输,就是要从降低延迟和恢复数据包时序入手。

  • 在发送端,为降低延迟,往往对传输数据进行预处理(降低质量和高效压缩)
  • 在接收端为了恢复时序,采用了接收缓冲;而为了实现媒体的流畅播放,则采用了播放缓冲

使用接收缓冲,可以将接收到的数据包缓存起来,然后根据数据包的封装信息(如包序号和时戳等),将乱序的包重新排序,最后将重新排序了的数据包放入播放缓冲播放。

为什么需要播放缓冲呢?容易想到,由于网络不可能很理想,并且对数据包排序需要处理时耗,我们得到排序好的数据包的时间间隔是不等的。如果不用播放缓冲,那么播放节目会很卡,这叫时延抖动(Jitter)。相反,使用播放缓冲,在开始播放时,花费几十秒钟先将播放缓冲填满(例如PPLIVE),可以有效地消除时延抖动,从而在不太损失实时性的前提下实现流媒体的顺畅播放。

3、RTP的协议层次

3.1、传输的子层

RTP(实时传输协议),顾名思义,它是用来提供实时传输的,因而可以看作是传输层的一个子层,下图给出了流媒体应用中的协议体系结构。

RTP被划分在传输层,它建立在UDP之上。同UDP协议一样,为了实现其传输功能,RTP也有固定的封装格式。UDP本身就是一种不可靠协议,所以建立在UDP之上的RTP就需要一个RTCP来提高一定的可靠性。

3.2、应用层的一部分

也有人把RTP归为应用层的一部分,这是从应用开发者的角度来说的。

操作系统中TCP/IP等协议所提供的是我们最常用的服务,而RTP的实现还是要考开发者自己。因此,从开发的角度来说,RTP的实现和应用层协议的实现没什么不同,所以将RTP看成应用层协议。

RTP实现者在发送RTP数据时,需先将数据封装成RTP包,而在接收到RTP数据包,需要将数据从RTP包中提取出来

4、RTP的封装

一个协议的封装时为了满足协议的功能需求,从前面提出的功能需求,可以推测RTP封装中应该有同步源和时间戳等信息。完整的RTP格式如下所示:

由上图中可知道RTP报文由两个部分构成:RTP报头 和 RTP有效负载,RTP头固定区一共12个字节

4.1、RTP头

版本号(V)

2B

RTP的版本号

填充标志(P)

1B

净荷末端是否包含一个或多个填充字节,净荷长度必须是4字节的倍数,因此需要填充字节使得净荷长度满足4字节倍数的要求,净荷的最后一个字节给出填充字节数

扩展位(X)

1B

RTP 首部扩展,RTP 首部扩展是可选的,若扩展位置1,表明在固定首部之后紧跟着一个首部扩展

提供源计数器(CC)

4B

RTP 首部中提供源标识符列表中的项数

标记(M)

1B

含义取决于携带净荷的类型,对于视频,标记一帧的结束;对于音频,标记会话的开始

有效载荷类型(PT)

7B

RTP 净荷的格式,通常,单个 RTP 分组所包含的净荷只能用一种净荷格式对多媒体数据进行编码

序列号(sequence number)

16B

会话开始时,发送端生成随机数作为初始值,传送 RTP 分组时,每传送一组该字段增 1,在会话存在期间,顺序发送的一串 RTP 分组的序号是递增的,接收端可以根据序号检测是否存在分组丢失或错序

时间戳(timestamp)

32B

净荷中第一个采样数据的采样时间,每一个采样数据的采样时间通过一个单调且线性递增的时钟获得,时钟频率取决于净荷数据的编码格式,相邻两个 RTP 分组的时间戳差值,取决于前一个 RTP 分组净荷中包含的采样数据数目

同步源标识符(SSRC)

32B

标明同步源,同步源是一个负责发送 RTP 分组并在 RTP 分组中设置序号和时间戳的实体,标识符是会话中全局惟一的,若 RTP 分组来自混合器则同步源标识符给出的是混合器的标识符

提供源标识符列表(CSRC)

(0~15)*32B

最多允许存在16个提供源标识符,若 RTP 分组来自混合器则提供源标识符列表给出进入混合器的各个信号的信号源标识符

下图为某网关上抓取的RTP报文:

填充字节(P)

如果P==1,那么就会在载荷末尾添加一个字节的填充位,组包的时候加上他,解包的时候跳过他 

int rtp_packet_des(struct rtp_packet_t *pkt, const void* data, int bytes) {
    // ......
    // padding
    if (1 == pkt->rtp.p)
    {
        uint8_t padding = ptr[bytes - 1];
        if (pkt->payloadlen < padding)  {
            assert(0);
            return -1;
        } else {
            pkt->payloadlen -= padding;
        }
    }
    // ......
}

标记(M)

  • RTP 包头中的 marker(M) 位用于标记媒体流中的关注事件,其确切含义由所使用的 RTP 配置文件和媒体类型定义
  • 对于在 RTP 配置文件下运行的音频流,标记位设置为 1 表示在一段静默期后发送的第一个数据包,否则设置为 0;将标记位设置为 1 可以告诉应用程序,这可能是调整播放点的好时机,因为听众通常不会注意到静音期长度的微小变化
  • 对于在 RTP 配置文件下运行的视频流,标记位设置为 1 表示视频帧的最后一个数据包,否则设置为 0;如果设置为 1,则该标记用作应用程序可以开始解码该帧的提示,而不是等待下一个具有不同时间戳的数据包来检测应显示该帧

有效载荷类型(PT)

序列号(sequence number)

  • RTP 序列号用于标识数据包,并在数据包丢失或乱序发送时向接收方发送标识,尽管它可以让接收者按发送顺序重新组织数据包,但它不用于安排数据包的播放顺序
  • 序列号的初始值应随机选择,而不是从零开始
  • 序列号应始终是连续的序列,对于每个发送的数据包,序列号应增加一,并且永远不要向前或向后跳转
  • 序列号是一个无符号的 16 位整数,每个包 +1,并在达到最大值时回绕为零,这意味着应用程序不应依赖序列号作为唯一的数据包标识符,建议使用扩展的序列号 (32 位或更宽) 在内部标识数据包,低 16 位是 RTP 数据包中的序列号,高 16 位是该序列号回绕的次数的计数

extended_seq_num = seq_num + (65536 * wrap_around_count) 

  • RTP 规范中用于维护回绕计数器的算法,RTP规范建议 max_misorder = 100, max_dropout = 3000
uint16_t udelta = seq – max_seq
if (udelta < max_dropout) {
    if (seq < max_seq) { 
        wrap_around_count++
    }
    max_seq = seq;
} else if (udelta <= 65535 – max_misorder) {
    // The sequence number made a very large jump
    if (seq == bad_seq) {
        // Two sequential packets received; assume the
        // other side has restarted without telling us
        ...
    } else {
        bad_seq = seq + 1;
    }
} else {
    // Duplicate or misordered packet
    ...
}

时间戳(timestamp)

  • RTP Timestamp 表示数据包中媒体数据的第一个 8 位字节的采样时刻,用于编排媒体数据的播出
  • 时间戳是一个 32 位无符号整数,其增加速率与媒体类型相关,并在超过最大值时回绕为零
  • 时间戳的初始值是随机选择的,而不是从零开始
  • 时间戳是从媒体时钟派生而来的,该媒体时钟必须以线性和单调的方式增加,从而为每个 RTP 会话生成单调的时间轴
  • 用于生成时间戳的媒体时钟的标称频率,由使用中的配置文件或有效负载格式定义
  • 对于具有静态有效负载类型分配的有效负载格式,使用静态有效负载类型时时钟频率是隐式的(指定为有效负载类型分配的一部分)
  • 对于具有动态有效负载类型分配的有效负载格式,分配过程必须指定频率以及有效负载类型,选择的速率必须满足所需的精度执行音视频同步并度量网络传输时间的变化
  • 时钟频率可能不会任意选择,大多数有效负载格式定义一个或多个可接受的速率
  • 时间戳的重复通常发生在将大视频帧拆分为多个 RTP 数据包进行传输时(这些数据包将具有不同的序号,但具有相同的时间戳)

同步源标识符(SSRC)

  • 指产生媒体流的信源,例如麦克风、摄像机、RTP 混合器等;通过 RTP 报头中的一个 32 位数字 SSRC 标识符来标识,而不依赖于网络地址,接收者将根据 SSRC 标识符来区分不同的信源,进行 RTP 报文的分组;
  • synchronization source (SSRC) 标识 RTP 会话中的参与者,它是一个临时的值,每个会话的标识符通过 RTP 控制协议映射到一个长期的规范名称即 CNAME
  • SSRC 是一个 32 位整数,由参与者加入会话时随机选择,选择了 SSRC 标识符后,参与者就可以在发送的数据包中使用该值,由于 SSRC 的值是在本地选择的,因此两个参与者可能会选择相同的值,当一个应用程序发现从另一个应用程序收到的数据包包含为其自身选择的 SSRC 标识符时,即可检测到此类冲突
  • 如果某个参与者检测到当前使用的 SSRC 与另一参与者选择的 SSRC 之间发生冲突,则它必须向原始 SSRC 发送 RTCP BYE 并选择另一个 SSRC,这种冲突检测机制可确保 SSRC 对于会话中的每个参与者都是唯一的
  • 具有相同 SSRC 的所有数据包均构成单个时序和序列号空间的一部分,因此接收方必须按 SSRC 对数据包进行分组才能进行播放
  • 如果参加者在一个 RTP 会话中生成多个流,每个流都必须标识为不同的 SSRC,以便接收方可以区分哪些数据包属于每个流

提供源标识符列表(CSRC)

  • 指当混合器接收到一个或多个同步信源的 RTP 报文后,经过混合处理产生一个新的组合 RTP 报文,并把混合器作为组合 RTP 报文的 SSRC,而将原来所有的 SSRC 都作为 CSRC 传送给接收者,使接收者知道组成组合报文的各个 SSRC;
  • 在正常情况下,RTP 数据由单个源生成,但是当多个 RTP 流通过混流器或转换器时,多个数据源可能对 RTP 数据包有所贡献,贡献源 (CSRC) 列表标识了对 RTP 数据包做出了贡献的参与者但不负责其时序和同步
  • 每个贡献源标识符都是一个 32 位整数,对应于对该数据包做出贡献的参与者的 SSRC
  • CSRC 列表的长度由 RTP 包头中的 CC 字段标识
  • 包含 CSRC 列表的数据包是通过 RTP 混流器的操作生成的,当接收到包含 CSRC 列表的数据包时,SSRC 将以常规方式将数据包分组以进行播放,并将每个 CSRC 添加到已知参与者列表中,由 CSRC 标识的每个参与者将具有相应的 RTP 控制协议数据包流,从而提供对参与者的完整标识
#define RTP_VERSION 2 // RTP version field must equal 2 (p66)

// RTP头固定12个字节
typedef struct
{
    uint32_t v:2;		/* protocol version */
    uint32_t p:1;		/* padding flag */
    uint32_t x:1;		/* header extension flag */
    uint32_t cc:4;		/* CSRC count */
    uint32_t m:1;		/* marker bit */
    uint32_t pt:7;		/* payload type */
    uint32_t seq:16;	/* sequence number */
    uint32_t timestamp; /* timestamp */
    uint32_t ssrc;		/* synchronization source */
} rtp_hdr_t;

// RTP头解析宏定义
#define RTP_V(v)	((v >> 30) & 0x03)      /* protocol version */
#define RTP_P(v)	((v >> 29) & 0x01)      /* padding flag */
#define RTP_X(v)	((v >> 28) & 0x01)      /* header extension flag */
#define RTP_CC(v)	((v >> 24) & 0x0F)      /* CSRC count */
#define RTP_M(v)	((v >> 23) & 0x01)      /* marker bit */
#define RTP_PT(v)	((v >> 16) & 0x7F)      /* payload type */
#define RTP_SEQ(v)	((v >> 00) & 0xFFFF)    /* sequence number */

#endif /* !_rtp_header_h_ */

4.2、RTP扩展头

其中X位如果为1,就表示CSRC后面还有一些额外的RTP扩展头,其格式如下:

但是这种形式只能够附加一个扩展头,为了支持多个扩展头,RFC5285以 defined by profile 进行了扩展。

One-Byte

RTP头后的第一个16位固定为 0XBEDE 标志,意味着这是一个one-byte扩展,length = 3 说明后面有三个扩展头,每个扩展头首先以一个byte开始,前4位是这个扩展头的ID, 后四位是data的长度-1,譬如说L=0意味着后面有1个byte的data,同理第二个扩展头的L=1说明后面还有2个byte的data,但是注意,其后没有紧跟第三个扩展头,而是添加了2个byte大小的全0的data,这是为了作填充对齐,因为扩展头是以为32bit作填充对齐的。

Two-Byte

RTP头后可以看到16位固定为 0x100 + 0x0, 接下来的为 length=3 表示接下来有3个头,接下来的就是扩展头和数据,扩展头除了ID和L相对于one-byte header从4bits变成了8bits之后,其余都一样。

4.3、RTP有效载荷

RTP Packet = RTP Header + RTP Payload。

视频封装(H.264)

RTP Payload结构一般分为3种:

  • 单NALU分组(Single NAL Unit Packet):一个分组只包含一个NALU。
  • 聚合分组(Aggregation Packet):一个分组包含多个NALU。
  • 分片分组(Fragmentation Unit):一个比较长的NALU分在多个RTP包中。

各种RTP分组在RTP Header后面跟着 F | NRI | Type 结构的NALU Header来判断分组类型。

  • F(forbidden_zero_bit):错误位或语法冲突标志,一般设为0。
  • NRI(nal_ref_idc): 与H264编码规范相同,此处可以直接使用原始码流NRI值。
  • Type:RTP载荷类型

  • 0:保留
  • 1-23:H264编码规定的数据类型,单NALU分组直接使用此值
  • 24-27:聚合分组类型(聚合分组一般使用24 STAP-A)
  • 28-29:分片分组类型(分片分组一般使用28FU-A)
  • 30-31:保留

单NALU(single nalu)

此结构的NALU Header结构可以直接使用原始码流NALU Header,所以单NALU分组 Type = 1~23。封装RTP包的时候可以直接把查询到的NALU去掉起始码(startcode)后的部分当作单NALU分组的RTP包Payload部分。

rtp_packet = rtp_header + nalu_header + 
    (nalu_data)

单时聚合分组(single-time aggregation)

通常采用STAP-A (Type=24)结构封装RTP聚合分组,下图为包含2个NALU的采用STAP-A结构的聚合分组。

  • STAP-A NALU Header(1):也是一个 NALU Header,Type = 24(STAP-A)。所有聚合 NALU 的 F 只要有一个为 1 则设为1,NRI 取所有 NALU 的 NRI 最大值;
  • NALU Size(2):表示此原始码流NALU长度;
  • NALU Header(1) + NALU Date:即为原始码流一个完整NALU;
rtp_packet = rtp_header + stap_a_nalu_header + 
    (nalu_size + nalu_header + nalu_data) + 
    (nalu_size + nalu_header + nalu_data)

STAP-A

STAP-B

// 单时聚合分组, 这里假设已经做完了排序
static int rtp_h264_unpack_stap(struct rtp_decode_h264_t *unpacker, const uint8_t* ptr, int bytes, uint32_t timestamp, int stap_b)
{
    int r, n;
    uint16_t len;
    uint16_t don;
    r = 0;
    // prt: 去除RTP包头的数据包
    // bytes: 数据包字节数
    // unpacker->ptr: 数据缓存
    // unpacker->size: 缓存字节数

    // STAP-A: STAP-A HDR(1) + (NALU SIZE(2) + NALU HDR(1) + NALU Data(size)) * n
    // STAP-B: STAP-A HDR(1) + DON(2) + (NALU SIZE(2) + NALU HDR(1) + NALU Data(size)) * n
    n = stap_b ? 3 : 1;
    if (bytes < n) {
        assert(0);
        return -EINVAL; // error
    }
    // DON,这里也就做做样子
    don = stap_b ? nbo_r16(ptr + 1) : 0;
    // 数据区
    ptr += n;

    // NALU循环
    for(bytes -= n; 0 == r && bytes > 2; bytes -= len + 2)
    {
        // NALU Size
        len = nbo_r16(ptr);
        if(len + 2 > bytes) {
            assert(0);
            unpacker->flags = RTP_PAYLOAD_FLAG_PACKET_LOST;
            unpacker->size = 0;
            return -EINVAL; // error
        }
#if 0
        assert(H264_NAL(ptr[2]) > 0 && H264_NAL(ptr[2]) < 24);
#endif
        // ptr+2: NALU HDR + NALU Data 
        r = unpacker->handler.packet(unpacker->cbparam, ptr + 2, len, timestamp, unpacker->flags);
        unpacker->flags = 0;
        unpacker->size = 0;

        // next NALU, 2表示NALU Size, len表示NALU HDR + NALU Data
        ptr += len + 2;
        // (don+1)%65536的意思是, 2^16的回绕
        don = (don + 1) % 65536;
    }
    return 0 == r ? 1 : r;
}

 多时聚合分组(multi-time aggregation)

MTAP-16

MTAP-24

分片分组(Fragmentation Unit)

通常采用无DON字段的 FU-A 结构封装 RTP 分片分组。各种 RTP 分组在 RTP Header 后面都跟着 NALU Header 结构,来判定分组类型。

  • FU Indicator(1):也是一个 NALU Header,Type = 28(FU-A)或 Type = 29(FU-B)。所有聚合 NALU 的 F 只要有一个为 1 则设为1,NRI 取所有 NALU 的 NRI 最大值,当Type=FU-B时,DON表示解码顺序。

  • FU Header(1):此结构中Type采用原始码流NALU中的 Type 字段(1~23)
  • S=1 表示这个RTP包为分片分组第一个分片
  • E=1 表示为分片分组最后一个分片
  • 除了首尾分片,中间的分片S&E都设为0,R为保留位,设为0;

  • NALU Header(1) + NALU Date:即为原始码流一个完整NALU;
rtp_packet = rtp_header + fu_a_nalu_header + 
    (fu_header + fu_payload)

FU-A

FU-B

// 5.8. Fragmentation Units (FUs) (p29)
// FU-A,FU-B:切片分组, 这里假设已经做完排序
static int rtp_h264_unpack_fu(struct rtp_decode_h264_t *unpacker, const uint8_t* ptr, int bytes, uint32_t timestamp, int fu_b)
{
    int r, n;
    uint8_t fu_header;
    r = 0;
    // prt: 去除RTP包头的数据包
    // bytes: 数据包字节数
    // unpacker->ptr: 数据缓存
    // unpacker->size: 缓存字节数

    // FU-A: FU INDI(1) + FU HDR(1) + FU Payload
    // FU-B: FU INDI(1) + FU HDR(1) + DON(2) + FU Payload
    n = fu_b ? 4 : 2;
    if (bytes < n || unpacker->size + bytes - n > RTP_PAYLOAD_MAX_SIZE) {
        assert(0);
        return -EINVAL; // error
    }

    // NALU总大小 > 解包容量
    if (unpacker->size + bytes - n + 1 > unpacker->capacity)
    {
        void* p = NULL;
        int size = unpacker->size + bytes + 1;
        size += size / 4 > 128000 ? size / 4 : 128000;
        p = realloc(unpacker->ptr, size);
        if (!p) {
            // set packet lost flag
            unpacker->flags = RTP_PAYLOAD_FLAG_PACKET_LOST;
            unpacker->size = 0;
            return -ENOMEM; // error
        }
        unpacker->ptr = (uint8_t*)p;
        unpacker->capacity = size;
    }
    // FU-HDR 第二个字节
    fu_header = ptr[1];

    // 首切包
    if (FU_START(fu_header)) {
#if 0
        if (unpacker->size > 0)
        {
            unpacker->flags |= RTP_PAYLOAD_FLAG_PACKET_CORRUPT;
            unpacker->handler.packet(unpacker->cbparam, unpacker->ptr, unpacker->size, unpacker->timestamp, unpacker->flags);
            unpacker->flags = 0;
            unpacker->size = 0;
        }
#endif
        // NALU Type
        unpacker->size = 1;
        // 清空缓存
        unpacker->ptr[0] = (ptr[0]/*indicator*/ & 0xE0) | (fu_header & 0x1F);
#if DEBUG
        assert(H264_NAL(unpacker->ptr[0]) > 0 && H264_NAL(unpacker->ptr[0]) < 24);
#endif
    } else {
        if (0 == unpacker->size) {
            unpacker->flags = RTP_PAYLOAD_FLAG_PACKET_LOST;
            return 0; // packet discard
        }
#if DEBUG
        assert(unpacker->size > 0);
#endif
    }
    // 时间戳,理论上应该是一样的
    unpacker->timestamp = timestamp;

    // 缓存追加
    // prt+n: 去除FuIndicator,FuHeader,DON
    if (bytes > n) {
#if DEBUG
        assert(unpacker->capacity >= unpacker->size + bytes - n);
#endif
        memmove(unpacker->ptr + unpacker->size, ptr + n, bytes - n);
        unpacker->size += bytes - n;
    }

    // 尾切包
    if(FU_END(fu_header))
    {
        // 多次传入数据后等到FU_END的时候得到一个完整的NALU
        if(unpacker->size > 0) {
            r = unpacker->handler.packet(unpacker->cbparam, unpacker->ptr, unpacker->size, timestamp, unpacker->flags);
        }
        unpacker->flags = 0;
        unpacker->size = 0; // reset
    }

    return 0 == r ? 1 : r; // packet handled
}

音频封装(AAC)

打包步骤:

  • 添加 RTP Header;
  • 添加 2 字节的 AU_HEADER_LENGTH;
  • 添加 2 字节的 AU_HEADER(并非固定2字节,具体要看SDP);
  • 从第 17 字节开始就是 Payload(去掉 ADTS 的 AAC 数据)

通常根据AAC码率大小可以分为 Low Bit-rate AAC 以及 High Bit-rate AAC 模式。

Low Bit-rate AAC

在Low Bit-rate AAC下规定的一帧大小最大不超过63字节

  • 在Low Bit-rate AAC模式下对应的SDP信息如下所示;
  • SDP中的mode=AAC-lbr,表示RTP封包的AAC采用Low Bit-rate AAC的模式;
  • sizeLength则表示AAC编码帧长这一参数占用的bit数,sizeLength=6则表示AAC帧长这一参数中占6bit所以编码帧长取值最大是63,即AAC编码帧长最大63字节;

High Bit-rate AAC

High Bit-rate AAC下规定一帧大小最大不超过8192字节

  • 在High Bit-rate AAC模式下对应的SDP信息如下;
  • SDP中mode=AAC-hbr,表示RTP封包的AAC采用High Bit-rate AAC的模式;
  • sizeLength则表示AAC编码帧长这一参数占用的bit数,sizeLength=13则表示AAC编码帧长这一参数中占13bit所以取值最大是8191,即AAC帧长最大8191字节;

一个RTP包中可以有一个 AU-headers-length 和 N 个 AU Header 和 N 个AU(AU是每包实际音频数据流)

AU-headers-length

头两个字节表示au-header的长度,单位是bit。一个AU-header长度是两个字节(16bit)因为可以有多个au-header,所以AU-headers-length的值是 16 的倍数,一般音频都是单个音频数据流的发送,所以AU-headers-length的值是16。

因为单位是bit,除以8就是auHeader的字节长度;又因为单个auheader字节长度2字节,所以再除以2就是auheader的个数。(注意:AU-header长度并不是固定为2字节,具体要看SDP)

AU-header

AU

AU就是去掉ADTS Header的AAC-ES。

抓包分析

RTP视频H.264和音频AAC的时间轴:

单时聚合分组

分片分组

4.4、基础代码

4.4.1、RTP解包 

// RTP解包
nt rtp_packet_deserialize(struct rtp_packet_t *pkt, const void* data, int bytes)
{
    uint32_t i, v;
    int hdrlen;
    const uint8_t *ptr;

    // RFC3550 5.1 RTP Fixed Header Fields(p12)
    if (bytes < RTP_FIXED_HEADER)
        return -1;
    ptr = (const unsigned char *)data;
    memset(pkt, 0, sizeof(struct rtp_packet_t));

    // pkt header
    v = nbo_r32(ptr);
    pkt->rtp.v = RTP_V(v);
    pkt->rtp.p = RTP_P(v);
    pkt->rtp.x = RTP_X(v);
    pkt->rtp.cc = RTP_CC(v);
    pkt->rtp.m = RTP_M(v);
    pkt->rtp.pt = RTP_PT(v);
    pkt->rtp.seq = RTP_SEQ(v);
    pkt->rtp.timestamp = nbo_r32(ptr + 4);
    pkt->rtp.ssrc = nbo_r32(ptr + 8);

#if DEBUG
    assert(RTP_VERSION == pkt->rtp.v);
#endif

    // rtp.cc表示CSRC个数
    // 假如rtp.cc > 0, 头长度需要带上CSRC的字节数
    hdrlen = RTP_FIXED_HEADER + pkt->rtp.cc * 4;
    if (RTP_VERSION != pkt->rtp.v || bytes < hdrlen + (pkt->rtp.x ? 4 : 0) + (pkt->rtp.p ? 1 : 0))
        return -1;
    // 保存SCRC
    for (i = 0; i < pkt->rtp.cc; i++) {
        pkt->csrc[i] = nbo_r32(ptr + 12 + i * 4);
    }
    // 获取Payload
    assert(bytes >= hdrlen);
    pkt->payload = (uint8_t*)ptr + hdrlen; // 跳过头部 拿到payload
    pkt->payloadlen = bytes - hdrlen; // payload长度

    // RTP Header Extension
    // 如果有扩展, rtp.payload需要偏移
	if (1 == pkt->rtp.x)
    {
        const uint8_t *rtpext = ptr + hdrlen;
        assert(pkt->payloadlen >= 4);
        pkt->extension = rtpext + 4;
pkt->reserved = nbo_r16(rtpext);
        pkt->extlen = nbo_r16(rtpext + 2) * 4;
        if (pkt->extlen + 4 > pkt->payloadlen) {
            assert(0);
            return -1;
        } else {
            pkt->payload = rtpext + pkt->extlen + 4;
            pkt->payloadlen -= pkt->extlen + 4;
        }
    }
    // RTP Header Padding
    if (1 == pkt->rtp.p)
    {
        uint8_t padding = ptr[bytes - 1];
        if (pkt->payloadlen < padding) {
            assert(0);
            return -1;
        } else {
            pkt->payloadlen -= padding;
        }
    }

    return 0;
}

4.4.2、RTP封包

// RTP封包
int rtp_packet_serialize(const struct rtp_packet_t *pkt, void* data, int bytes)
{
    int hdrlen;

    hdrlen = rtp_packet_serialize_header(pkt, data, bytes);
    if (hdrlen < RTP_FIXED_HEADER || hdrlen + pkt->payloadlen > bytes)
        return -1;

    memcpy(((uint8_t*)data) + hdrlen, pkt->payload, pkt->payloadlen);
    return hdrlen + pkt->payloadlen;
}

// RTP头打包
int rtp_packet_serialize_header(const struct rtp_packet_t *pkt, void* data, int bytes)
{
    int hdrlen;
    uint32_t i;
    uint8_t* ptr;

    if (RTP_VERSION != pkt->rtp.v || 0 != (pkt->extlen % 4)) {
        assert(0); // RTP version field must equal 2 (p66)
        return -1;
    }

    // RFC3550 5.1 RTP Fixed Header Fields(p12)
    // 对于CC值计算CSRC的长度扩展
    // 对于X扩展计算Padding的长度扩展
    hdrlen = RTP_FIXED_HEADER + pkt->rtp.cc * 4 + (pkt->rtp.x ? 4 : 0);
    if (bytes < hdrlen + pkt->extlen)
        return -1;

    ptr = (uint8_t *)data;
    nbo_write_rtp_header(ptr, &pkt->rtp);
    ptr += RTP_FIXED_HEADER;

    // CSRC列表封装到头部
    for (i = 0; i < pkt->rtp.cc; i++, ptr += 4) {
        nbo_w32(ptr, pkt->csrc[i]);
    }

    // RTP Header Extension
    if (1 == pkt->rtp.x)
    {
        // 5.3.1 RTP Header Extension
        assert(0 == (pkt->extlen % 4));
        nbo_w16(ptr, pkt->reserved);
        nbo_w16(ptr + 2, pkt->extlen / 4);
        memcpy(ptr + 4, pkt->extension, pkt->extlen);   // extension封装到头部
        ptr += pkt->extlen + 4;
    }
    return hdrlen + pkt->extlen;
}

有关流媒体基础-RTP协议的更多相关文章

  1. 怎样用一台手机做自媒体? - 2

    其实做自媒体的成本并不高,入门只需要一部手机即可!在手机上找视频素材、使用手机剪辑视频、最后使用手机发布视频作品获得收益!方法并不难,今天这期内容就来给粉丝们分享一种小方法,每天稳定收益100-300,抓紧点赞收藏!1、找素材(1)使用手机拍摄自己喜欢的经典段落,使用程序把文案内容提取出来(2)也可以在豆瓣、知乎、微博等网站中找一些自己需要的文案素材(3)把文案进行润色修改,可以加入一些自己的观点(4)视频素材可以使用软件中自带的素材,也可以在素材网站中下载完整版的素材2、文案配音(1)把复制好的文案直接导入小程序中(2)调整音色、音调后一键合成音频即可(3)可以选择自己朗读配音,需要花一点时

  2. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  3. 软件测试基础 - 2

    Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功

  4. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  5. ES基础入门 - 2

    ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear

  6. ruby - HTTP POST 上的 SSL 错误(未知协议(protocol)) - 2

    尝试通过SSL连接到ImgurAPI时出现错误。这是代码和错误:API_URI=URI.parse('https://api.imgur.com')API_PUBLIC_KEY='Client-ID--'ENDPOINTS={:image=>'/3/image',:gallery=>'/3/gallery'}#Public:Uploadanimage##args-Theimagepathfortheimagetoupload#defupload(image_path)http=Net::HTTP.new(API_URI.host)http.use_ssl=truehttp.verify

  7. 【网络】-- 网络基础 - 2

    (本文是网络的宏观的概念铺垫)目录计算机网络背景网络发展认识"协议"网络协议初识协议分层OSI七层模型TCP/IP五层(或四层)模型报头以太网碰撞路由器IP地址和MAC地址IP地址与MAC地址总结IP地址MAC地址计算机网络背景网络发展        是最开始先有的计算机,计算机后来因为多项技术的水平升高,逐渐的计算机变的小型化、高效化。后来因为计算机其本身的计算能力比较的快速:独立模式:计算机之间相互独立。    如:有三个人,每个人做的不同的事物,但是是需要协作的完成。    而这三个人所做的事是需要进行协作的,然而刚开始因为每一台计算机之间都是互相独立的。所以前面的人处理完了就需要将数据

  8. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  9. 网络实验之RIPV2协议(一) - 2

    一、RIPV2协议简介  RIP(RoutingInformationProtocol)路由协议是一种相对古老,在小型以及同介质网络中得到了广泛应用的一种路由协议。RIP采用距离向量算法,是一种距离向量协议。RIP-1是有类别路由协议(ClassfulRoutingProtocol),它只支持以广播方式发布协议报文。RIP-1的协议报文无法携带掩码信息,它只能识别A、B、C类这样的自然网段的路由,因此RIP-1不支持非连续子网(DiscontiguousSubnet)。RIP-2是一种无类别路由协议(ClasslessRoutingProtocol),支持路由标记,在路由策略中可根据路由标记对

  10. 【Elasticsearch基础】Elasticsearch索引、文档以及映射操作详解 - 2

    文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就

随机推荐