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进行梳理
相关知识和资料来源:推荐免费订阅