音视频学习之rtsp学习rtp协议的理解(rtp)

1:理论理解相关细节

实际的媒体数据(视频/音频)的传输是通过rtp进行传输的。

rtp可以基于udp进行发送,也可以基于tcp进行发送。 (这个有点疑问,看很多都说rtp是基于udp传输)

==》那么乱序,丢包,以及一个图片资源过大,如何拆包相关逻辑呢

rtp传输h264 图像资源,需要了解h264格式数据相关知识,以及如何进行封包发送以及接收后解包处理

rtp传输AAC 音频文件,需要了解aac相关格式(aac有两种格式),同样思考如何封包以及解包。

在进行rtsp测试的时候,发现音频如果按定时器发送帧,会有声音卡顿的现象,这里如何做一些处理呢?

上一文中有个疑问,使用rtp进行推流,如何播放没有成功,依然有疑问,但至少:

==》rtp包中只是部分的数据,而音视频的播放还需要知道一些其他信息(sdp),如当前流的类型,播放音频时的采样率等一些必要信息

==》1:使用rtp进行推流时,获取一个sdp文件,使用该文件进行拉流播放。

==》2:使用rtp接收到数据后,进行相关的解析后,存储到本地后,进行播放。

上一文中有一个疑问,不知道怎么用obs进行推流:

==》obs是一个强大的视频直播录制软件,可以支持推流功能。
==》obs可以采集摄像头,音频,桌面,窗口等功能,这里只关注测试推流
==》推流时,需要设置,在 文件–>设置–>推流中进行设置,填写我们的服务器相关,然后选择一些采集方式点击开始推流(测试成功):
在这里插入图片描述

2:了解rtp相关协议(根据课程已有代码)

2.1:概念了解一下:

rtp实时传输协议,是传输层协议(通常基于udp(实时传输))

===》实际传输:最大传输单元MTU需要考虑

rtp实际内部传输的是实际流数据(可以是一帧完整的(如音频帧),可能不足一帧(如图像资源))

rtp内部实际数据流可以是各种格式的数据,如h264,aac,以及其他协议。

rtp可以支持传输多种流,如一个rtp链接可以同时传输h264和aac的流。

===》如果rtp支持传输多种格式,支持传输多种流等,需要在实际传输前做一定的信息协商(sdp)

rtp协议通常和rtcp协议一起使用。

rtp over rtsp(udp)和rtp over rtsp(tcp)之间的理解

===》rtp是传输层协议,但本质上说其实还是应用层协议,只是相对应用层协议更底层

===》rtp和rtcp即可以用udp进行传输,也可以用tcp进行传输。

===》rtsp涉及多组传输通道,要定义rtp传输的端口。
在这里插入图片描述

2.2:协议理解一下

阅读RFC3550中文文档时,rtp的使用场景可以有:多播音频会议,音频和视频会议,混频器,转换器,分层编码,监视器等

rtcp 是rtp控制协议,如会议中人数的增加与离开,混频是相关格式与rtp进行适配,计算当前带宽,控制rtp的发送频率等

===》rtcp本身也占带宽,其发送的频率也有一定的规范

===》rtcp有不同的包类型:SR(发送者报告),RR(接收者报告),SDES(源描述项),BYE(会话结束),APP(应用描述功能)

2.2.1:rtp报文格式

在这里插入图片描述

2.2.2:rtp报文格式简单描述

前 12 个字节出现在每个 RTP 包中,仅仅在被混合器插入时,才出现 CSRC 识别符列表

版本(V):RTP协议的版本号,占2位,当前协议版本号为2。

填充 ( P):填充标志 占1位,如果P=1,则在该报⽂的尾部填充⼀个或多个额外的⼋位组,它们不是有效载荷 的⼀部分。(可能用于某些 具有固定长度的加密算法,或者用于在底层数据单元中传输多个 RTP 包。)

==》如设置填充位,在包尾将包含附加填充字,它不属于有效载荷。填充字节长度位最后一个字节的值。某些加密算法需要固定大小的填充字,或为在底层协议数据单元中携带几个RTP包。

扩展(X):扩展比特,占1位,如果X=1,则固定头(仅)后面跟随一个头扩展。(扩展头有固定的头格式0XBEDE标志开始,并且有32位对齐)

CSRC 计数(CC):CSRC 计数器,占4位,包含了跟在固定头后面 CSRC 识别符的数目。

标志(M):标记,占1位,不同的有效载荷有不同的含义,

==》对于视频,标记⼀帧的结束;对于⾳频,标记帧的开 始。

负载类型(PT):有效载荷类型,占7位,⽤于说明RTP报⽂中有效载荷的类型,如GSM⾳频、JPEM图像等。

序列号(sequence number)占16位,⽤于标识发送者所发送的RTP报⽂的序列号,每发送⼀个报⽂,序列号增1。

==》接收者 通过序列号来检测报⽂丢失情况,重新排序报⽂,恢复数据。(初始值随机)

时间戳(timestamp):占32位,时间戳反映了该RTP报⽂的第⼀个⼋位组(第一个字节)的采样时刻。

==》接收者使⽤时间戳来 计算延迟和延迟抖动,并进⾏同步控制。

==》时钟频率依赖于负载数据格式,并在描述文件(profile)中进行描述

==》如果 RTP 包是周期性产生的,那么将使用由采样时钟决定的名义上的采样时刻,而不是读取系统时间(对一个固定速率的音频,采样时钟将在每个周期内增加 1。如果一个音频从 输入设备中读取含有 160 个采样周期的块,那么对每个块,时间戳的值增加 160)

==》时间戳的初始值应当是随机的,就像序号一样。几个连续的 RTP 包如果是同时产生的,将有相同的序列号。

==》如果传输的数据是存贮好的,而不是实时采样等到的,那么会使用从参考时钟得到的虚的 表示时间线。

同步信源(SSRC):占32位,⽤于标识同步信源。

==》同步源,所有相同标识的源,一起进行处理。

==》一个同步源的所有包构成了相同计时和序列号 空间的一部分,这样接收方就可以把一个同步源的包放在一起,来进行重放。

==》该标识符是随机选择的,参加同⼀视频会议的 两个同步信源不能有相同的SSRC(要解决冲突)。

==》如麦克风、摄影机、RTP 混频器(见下文)就是同步源

==》一个同步源可能随着时间变化而改变其数据格式,如音频编码。

特约信源(CSRC):每个CSRC标识符占32位,可以有0~15个。(是一个表)

==》作用源,组成混合器中所有起作用的源。

==》个数由CSRC 计数(CC)决定

==》CSRC表:标识了包含在该RTP 报⽂有效载荷中的所有特约信源。

==》由混合器插入,列出所有的混合器中的作用信源。

==》例如音频会议中,哪些人说话被组合在包中,可以让接听者知道谁在说话。

2.2.2:rtp报文头定义

typedef struct _rtp_header_t
{
    uint32_t v:2;		/* 版本          占2位  2*/
    uint32_t p:1;		/* 填充标志       占1位 加密或者多个rtp包时用???*/
    uint32_t x:1;		/* 扩展标志       占1位 增加头扩展,有固定的格式,32位对齐 */
    uint32_t cc:4;		/* CSRC计数器     占4位 作用源的个数*/
    uint32_t m:1;		/* 标志 			占1位 视频标志结束,音频标志开始*/
    uint32_t pt:7;		/* 有效载荷,类型   占7位 如GSM⾳频、JPEM图像等*/
    uint32_t seq:16;	/*序列号         占16位 丢包重排恢复数据用*/
    uint32_t timestamp; /*时间戳         占16位 进行延迟控制  */
    uint32_t ssrc;		/*同步源     占32位    同一标识多个同步源一起处理 */
    					/*作用源     占32位    混合器情况下才有,这里没加。*/
} rtp_header_t;   

2.3:对应代码理解一下

作为一个协议,从以下几点理解:

1:根据协议定义结构体

2:构造协议报文,序列化

3:解析协议报文,反序列化

这里简单根据测试源码,对这几个细节做梳理:

2.3.1:头结构

//头结构
typedef struct _rtp_header_t
{
    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_header_t;


struct rtp_packet_t     // 封装这个RTP 包括 header + [csrc/extension] + payload
{
    rtp_header_t rtp;
    uint32_t csrc[16];      // 最多16个csrc
    const void* extension; // extension(valid only if rtp.x = 1)
    uint16_t extlen; // extension length in bytes
    uint16_t reserved; // extension reserved
    const void* payload; //  rtp payload
    int payloadlen; // payload length in bytes
};

2.3.2:构造要发送的rtp包

这里的函数实际上是已经有的rtp包,仅仅是做处理进行发送,其他逻辑后续整理。

//根据rtpt头部数据 rtp_header_t 结构,写入ptr中
static inline void nbo_write_rtp_header(uint8_t *ptr, const rtp_header_t *header)
{
    ptr[0] = (uint8_t)((header->v << 6) | (header->p << 5) | (header->x << 4) | header->cc);
    ptr[1] = (uint8_t)((header->m << 7) | header->pt);
    ptr[2] = (uint8_t)(header->seq >> 8);
    ptr[3] = (uint8_t)(header->seq & 0xFF);

    nbo_w32(ptr+4, header->timestamp);
    nbo_w32(ptr+8, header->ssrc);
}

// 把可读RTP packet封装成要发送出去的数据 序列化
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)
    hdrlen = RTP_FIXED_HEADER + pkt->rtp.cc * 4 + (pkt->rtp.x ? 4 : 0);
    if (bytes < hdrlen + pkt->extlen)
        return -1;

    ptr = (uint8_t *)data;
    //写入rtp_header_t 相关数据 包括时间戳和ssrc
    nbo_write_rtp_header(ptr, &pkt->rtp);
    ptr += RTP_FIXED_HEADER;

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

    // pkt header extension
    //如果有扩展标志,写入再rtp头后面
    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;
}

//data位最终要发送的数据 
int rtp_packet_serialize(const struct rtp_packet_t *pkt, void* data, int bytes)
{
    int hdrlen;

    //把rtp包头数据写入data
    hdrlen = rtp_packet_serialize_header(pkt, data, bytes);
    if (hdrlen < RTP_FIXED_HEADER || hdrlen + pkt->payloadlen > bytes)
        return -1;

    //把实际的payload写入data
    memcpy(((uint8_t*)data) + hdrlen, pkt->payload, pkt->payloadlen);
    //返回整个data的实际大小
    return hdrlen + pkt->payloadlen;
}

2.3.3:解析收到rtp包

//获取到的rtp包进行解析的逻辑 注意填充为和标志位的处理
/*
 0               1               2               3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|   CC  |M|     PT      |      sequence number          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                synchronization source (SSRC) identifier       |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                 contributing source (CSRC) identifiers        |
|                               ....                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
// 通过收到的数据,解析出来可读的RTP packet 反序列化 bytes是收到的字节序
int rtp_packet_deserialize(struct rtp_packet_t *pkt, const void* data, int bytes)
{
    uint32_t i, v;
    int hdrlen;
    const uint8_t *ptr;

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

    // pkt header  网络字节序的处理
    v = nbo_r32(ptr);   //uint32 处理前4个字节
    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);  //处理接下来的4个字节 即 timestamp
    pkt->rtp.ssrc = nbo_r32(ptr + 8);       //SSRC  
    assert(RTP_VERSION == pkt->rtp.v);      // 调试的时候用

    hdrlen = RTP_FIXED_HEADER + pkt->rtp.cc * 4;    // 解析带csrc时的总长度
    //根据rtcp头数据进行校验      版本  头长度以及扩展标志和填充标志
    if (RTP_VERSION != pkt->rtp.v || bytes < hdrlen + (pkt->rtp.x ? 4 : 0) + (pkt->rtp.p ? 1 : 0))
        return -1;      // 报错

    // pkt contributing source
    //如果有作用源相关信息 获取 CSRC 表
    for (i = 0; i < pkt->rtp.cc; i++)
    {
        pkt->csrc[i] = nbo_r32(ptr + 12 + i * 4);
    }

    assert(bytes >= hdrlen);
    pkt->payload = (uint8_t*)ptr + hdrlen;      // 跳过头部 拿到payload
    pkt->payloadlen = bytes - hdrlen;           // payload长度

    // pkt header extension
    //如果有扩展标志 
    if (1 == pkt->rtp.x)
    {
        const uint8_t *rtpext = ptr + hdrlen;
        assert(pkt->payloadlen >= 4);
        //rtp扩展头也是特定的格式
        pkt->extension = rtpext + 4; //这里应该是扩展头特定标识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;
        }
    }

    // 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;
}

3:总结及下一步

看到相关的文档,rtp属于传输层协议,都是基于udp传输的,但是又理解到有时候rtp可以通过tcp的方式进行传输,这是遗留的一点疑问。

下一步:

rtp如何与相对应的h264,aac等文件格式交互的? 梳理一个读取h264的文件并进行推流的流程。

rtcp报文涉及SR,RR,SDES,BYE,APP不通类型的报文,以及rtcp在整个业务流程中的控制作用及细节梳理。

分析rtp的测试源码,使用rtp进行传输h264和aac进行梳理

相关知识和资料来源:推荐免费订阅

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值