RTP协议
1、RTP概述
实时传输协议(Real-time Transport Protocol)是一种网络传输协议。为IETF提出的一个标志,对应的RFC文档为RFC3550(RFC1889为过期版本)。RFC3550不仅定义了RTP,而且定义了配套的相关协议RTCP(Real-time Transport Control Protocol,实时传输控制协议)。RTP用来为IP网络上的语音、图像、传真等多种需要实时传输的多媒体数据提供端到端的实时传输服务。RTP为Internet上端到端的实时传输提供时间信息和流同步,但并不保证服务质量,服务质量由RTCP来提供。
2、RTP应用环境
RTP用于在单播或多播网络中传送实时数据。它们典型的应用场合如下:
- 简单的多播音频会议。语音通信通过一个多播地址和一对端口来实现。一个端口用于音频数据(RTP),一个端口用于控制包(RTCP)。
- 音频和视频会议。如果在一次会议中同时使用了音频和视频会议,这两种媒体将分别在不同的RTP会话中传送,每个会话使用不同的传输地址(IP地址+端口)。如果一个用户同时使用了两个会话,则每个会话对应的RTCP包都使用规范化名字CNAME(Canonical Name)。与会者可以根据RTCP包中的CNAME来获取相关联的音频和视频,然后根据RTCP包中的计时信息(Network time protocal)来实现音频和视频的同步。
- 翻译器和混合器。翻译器和混合器都是RTP级的中继系统。翻译器用在通过IP多播不能直接到达用户区,例如发送者和接收者之间存在防火墙。当与会者能接收的音频编码格式不一样,比如有一个与会者通过一条低速链路接入高速会议,这是就要使用混合器。在进入音频数据格式需要变化的网络前,混合器将来自一个源或多个源的音频包进行重构,并把重构后的多个音频合并,采用另一种音频编码进行编码后,再转发这个新的RTP包。从一个混合器出来的所有数据包要用混合器作为它们的同步源(SSRC,见RTP封装)来识别,可以通过贡献源列表(CSRC表,见RTP的封装)可确认谈话者。
3、流媒体
流媒体是指Internet上使用流式传输技术的连续时基媒体。当前在Internet上传输音频和视频等信息主要有两种方式:下载和流式传输两种方式。
下载情况下,用户需要先下载整个媒体文件到本地,然后才能播放媒体文件。在视频直播等应用场合,由于生成整个媒体文件要等直播结束,也就是用户至少要在直播结束后才能看到直播节目,所以用下载方式不能实现直播。
流式传输是实现流媒体的关键技术。使用流式传输可以边下载边观看流媒体节目。由于Internet是基于分组传输的,所以接收端收到的数据包往往有延迟和乱序(流式传输构建在UDP上)。要实现流式传输,就是要从降低延迟和恢复数据包时序入手。在发送端,为降低延迟,往往对传输数据进行预处理(降低质量和高效压缩)。在接收端为了恢复时序,采用了接收缓冲;而为了实现媒体的流畅播放,则采用了播放缓冲。
使用接收缓冲,可以将接收到的数据包缓存起来,然后根据数据包的封装信息(如包序号和时戳等),将乱序的包重新排序,最后将重新排序了的数据包放入播放缓冲播放。
为什么需要播放缓冲呢?容易想到,由于网络不可能很理想,并且对数据包排序需要处理时耗,我们得到排序好的数据包的时间间隔是不等的。如果不用播放缓冲,那么播放节目会很卡,这叫时延抖动。相反,使用播放缓冲,在开始播放时,花费几十秒钟先将播放缓冲填满(例如PPLIVE),可以有效地消除时延抖动,从而在不太损失实时性的前提下实现流媒体的顺畅播放。
到目前为止,Internet 上使用较多的流式视频格式主要有以下三种:RealNetworks 公司的RealMedia ,Apple 公司的QuickTime 以及Microsoft 公司的Advanced Streaming Format (ASF) 。
上面在谈接收缓冲时,说到了流媒体数据包的封装信息(包序号和时戳等),这在后面的RTP封装中会有体现。另外,RealMedia这些流式媒体格式只是编解码有不同,但对于RTP来说,它们都是待封装传输的流媒体数据而没有什么不同。
4、RTP的协议层次
4.1 传输的子层
RTP(实时传输协议),顾名思义,它是用来提供实时传输的,因而可以看作是传输层的一个子层,下图给出了流媒体应用中的协议体系结构。
从上图可以看出,RTP被划分在传输层,它建立在UDP之上。同UDP协议一样,为了实现其传输功能,RTP也有固定的封装格式。
4.2 应用层的一部分
也有人把RTP归为应用层的一部分,这是从应用开发者的角度来说的。操作系统中TCP/IP等协议所提供的是我们最常用的服务,而RTP的实现还是要考开发者自己。因此,从开发的角度来说,RTP的实现喝应用层协议的实现没不同,所有将RTP看成应用层协议。
RTP实现者在发送RTP数据时,需先将数据封装成RTP包,而在接收到RTP数据包,需要将数据从RTP包中提取出来。
5、RTP的封装
一个协议的封装时为了满足协议的功能需求,从前面提出的功能需求,可以推测RTP封装中应该有同步源喝时间戳等信息。完整的RTP格式如下所示:
上图引自RFC3550。
由上图中可知道RTP报文由两个部分构成:RTP报头和RTP有效负载。报头格式如上图所示,其中:
- V:RTP协议的版本号,占2位,当前协议版本号为2。
- P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
- X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头。
- CC:CSIC计数器,占4位,指示CSIC 标识符的个数。
- M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
- PT: 有效载荷类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。
- 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,在helix服务器中这个字段是从0开始的,同时音频包和视频包的sequence是分别记数的。
- 时戳(Timestamp):占32位,时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。
- 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
- 贡献信源(CSRC)标识符:贡献源列表,0~15项,每个项32bit,用来标志对一个RTP混合器产生的新包所有RTP包的源。由混合器将这些由贡献的SSRC标志符插入表中。SSRC标志符都被列出来,以便接收端能正确指出交谈双方的身份。
如果扩展标志被置位则说明紧跟在报头后面是一个头扩展,其格式如下:
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 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | defined by profile | length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | header extension | | .... |
下图为某网关上抓取的RTP报文:
参数说明:
v=2
p=0
x=0
cc=0
m=0
pt=ITU-T G.711 PCMU(0) //即pcmu语音编码
seq=8004
timestamp=1157612680
ssrc=0xcd934761
payload=27... //为实际负载。
并且可以看到,RTP报文由UDP报文承载,UDP由IP报文承载。
6、RTCP的封装
RTP需要RTCP为其服务器质量提供保证。RTCP的主要功能是:服务质量的监视与反馈、媒体间的同步,以及多播组中成员的标识。在RTP会话期间,各参与者周期性地传送RTCP包。RTCP包中含有已发送的数据包的梳理、丢失的数据包数量等统计资料。因此,各参与者可以利用这些信息动态地修改传输速率,甚至改变有效载荷类型。RTP和RTCP配合使用,它们能以有效的反馈和最小的开销使传输效率最佳化,因而特别适合传送网上的实时数据。
RTCP也是用UDP报文来传送的,但RTCP封装的仅仅是一些控制信息,因而分组很短,所有可以将多个RTCP分组封装在一个UDP报文中。RTCP由如下五种分组类型:
上述五种分组的封装大同小异,下面只讲诉SR类型,其他类型请参考RFC5330.
发送端报告分组SR(Sender Report)用来使发送端以多播方式向所有接收端报告发送情况。SR分组的主要内容有:相应的RTP流的SSRC,RTP流中最新产生的RTP分组的时间戳和NTP,RTP流包含的分组数,RTP流包含的字节数。SR包的封装如下所示。
- 版本(V):同RTP包头域。
- 填充(P):同RTP包头域。
- 接收报告计数器(RC):5比特,该SR包中的接收报告块的数目,可以为零。
- 包类型(PT):8比特,SR包是200。
- 长度域(Length):16比特,其中存放的是该SR包以32比特为单位的总长度减一。
- 同步源(SSRC):SR包发送者的同步源标识符。与对应RTP包中的SSRC一样。
- NTP Timestamp(Network time protocol)SR包发送时的绝对时间值。NTP的作用是同步不同的RTP媒体流。
- RTP Timestamp:与NTP时间戳对应,与RTP数据包中的RTP时间戳具有相同的单位和随机初始值。
- Sender’s packet count:从开始发送包到产生这个SR包这段时间里,发送者发送的RTP数据包的总数. SSRC改变时,这个域清零。
- Sender`s octet count:从开始发送包到产生这个SR包这段时间里,发送者发送的净荷数据的总字节数(不包括头部和填充)。发送者改变其SSRC时,这个域要清零。
- 同步源n的SSRC标识符:该报告块中包含的是从该源接收到的包的统计信息。
- 丢失率(Fraction Lost):表明从上一个SR或RR包发出以来从同步源n(SSRC_n)来的RTP数据包的丢失率。
- 累计的包丢失数目:从开始接收到SSRC_n的包到发送SR,从SSRC_n传过来的RTP数据包的丢失总数。
- 收到的扩展最大序列号:从SSRC_n收到的RTP数据包中最大的序列号。
- 接收抖动(Interarrival jitter):RTP数据包接受时间的统计方差估计。
- 上次SR时间戳(Last SR,LSR):取最近从SSRC_n收到的SR包中的NTP时间戳的中间32比特。如果目前还没收到SR包,则该域清零。
- 上次SR以来的延时(Delay since last SR,DLSR):上次从SSRC_n收到SR包到发送本报告的延时。
图为某网关上抓取的RTCP报文:
7、RTP会话过程
当应用程序建立一个RTP会话时,应用程序将确定一对目的传输地址。目的传输地址由一个网络地址和一对端口组成,有两个端口:一个给RTP包,一个给RTCP包,使得RTP/RTCP数据能够正确发送。RTP数据发向偶数的UDP端口,而对应的控制信号RTCP数据发向相邻的奇数UDP端口(偶数的UDP端口+1),这样就构成一个UDP端口对。 RTP的发送过程如下,接收过程则相反。
- RTP协议从上层接收流媒体信息码流(如H.263),封装成RTP数据包;RTCP从上层接收控制信息,封装成RTCP控制包。
- RTP将RTP 数据包发往UDP端口对中偶数端口;RTCP将RTCP控制包发往UDP端口对中的接收端口。
8、RTP Payload
RTP Packet = RTP Header + RTP Payload。
RTP Payload结构一般分为3种:
- 单NALU分组(Single NAL Unit Packet):一个分组只包含一个NALU。
- 聚合分组(Aggregation Packet):一个分组包含多个NALU。
- 分片分组(Fragmentation Unit):一个比较长的NALU分在多个RTP包中。
各种RTP分组在RTP Header后面跟着F|NRI|Type结构的NALU Header来判断分组类型。不容分组类型此字段名字可能不同,H264/HEVC原始视频流NALU也包含此结构的头部字段。
- F(forbidden_zero_bit):错误位或语法冲突标志,一般设为0。
- NRI(nal_ref_idc): 与H264编码规范相同,此处可以直接使用原始码流NRI值。
- Type:RTP载荷类型,1-23:H264编码规定的数据类型,单NALU分组直接使用此值,24-27:聚合分组类型(聚合分组一般使用24 STAP-A),28-29分片分组类型(分片分组一般使用28FU-A),30-31,0保留。
8.1 单NALU分组
此结构的NALU Header结构可以直接使用原始码流NALU Header,所以单NALU分组Type = 1~23。封装RTP包的时候可以直接把 查询到的NALU去掉起始码后的部分 当作单NALU分组的RTP包Payload部分。
8.2 聚合分组
通常采用STAP-A (Type=24)结构封装RTP聚合分组,下图为包含2个NALU的采用STAP-A结构的聚合分组。
- STAP-A NAL HDR: 也是一个NALU Header (F|NRI|Type)结构,1字节。比如可能值为0x18=00011000b,Type=11000b=24,即为STAP-A。所有聚合NALU的F只要有一个为1则设为1,NRI取所有NALU的NRI最大值。
- NALU Size: 表示此原始码流NALU长度,2字节。
- NALU HDR + NALU Date: 即为原始码流一个完整NALU。
8.3 分片分组
通常采用无DON字段的FU-A结构封装RTP分片分组。各种RTP分组在RTP Header后面都跟着 F|NRI|Type 结构,来判定分组类型。
FU indicator
采用FU-A分组类型的话Type = 28,NRI与此NALU中NRI字段相同。
FU header
此结构中Type采用原始码流NALU中的Type字段,S=1表示这个RTP包为分片分组第一个分片,E=1表示为分片分组最后一个分片。除了首尾分片,中间的分片S&E都设为0。R为保留位,设为0。
9、RTP封装H.264码流示例程序
这个示例程序是参考ffmpeg的代码,实现了读取一个Sample.h264裸流文件,(打算以后支持HEVC/H.265所以文件名有HEVC),通过ffmpeg内置的函数查找NAL单元起始码,从而获取一个完整的NALU。根据NALU长度选择RTP打包类型,然后再组装RTP头部信息,最终发送到指定IP和端口,本例发送到本机1234端口。
程序文件:
- main.c: 函数入口
- RTPEnc.c: RTP封装实现
- Network.c: UDP socket相关
- AVC.c: 查找NALU起始码函数,copy自ffmpeg
- Utils: 读取文件以及copy指定长度的内存数据
main.c
#include <stdio.h> #include <string.h> #include "Utils.h" #include "RTPEnc.h" #include "Network.h" int main() { int len = 0; int res; uint8_t *stream = NULL; const char *fileName = "../Sample.h264"; RTPMuxContext rtpMuxContext; UDPContext udpContext = { .dstIp = "127.0.0.1", // 目的IP .dstPort = 1234 // 目的端口 }; // 读整个文件到buff中 res = readFile(&stream, &len, fileName); if (res){ printf("readFile error.\n"); return -1; } // create udp socket res = udpInit(&udpContext); if (res){ printf("udpInit error.\n"); return -1; } // 设置RTP Header默认参数 initRTPMuxContext(&rtpMuxContext); // 主要业务逻辑 rtpSendH264HEVC(&rtpMuxContext, &udpContext, stream, len); return 0; }
RTPEnc.h
#ifndef RTPSERVER_RTPENC_H #define RTPSERVER_RTPENC_H #include "Network.h" #define RTP_PAYLOAD_MAX 1400 typedef struct { uint8_t cache[RTP_PAYLOAD_MAX+12]; //RTP packet = RTP header + buf uint8_t buf[RTP_PAYLOAD_MAX]; // NAL header + NAL uint8_t *buf_ptr; int aggregation; // 0: Single Unit, 1: Aggregation Unit int payload_type; // 0, H.264/AVC; 1, HEVC/H.265 uint32_t ssrc; uint32_t seq; uint32_t timestamp; }RTPMuxContext; int initRTPMuxContext(RTPMuxContext *ctx); /* send a H.264/HEVC video stream */ void rtpSendH264HEVC(RTPMuxContext *ctx, UDPContext *udp, const uint8_t *buf, int size); #endif //RTPSERVER_RTPENC_H
RTPEnc.c
#include <stdint.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include "RTPEnc.h" #include "Utils.h" #include "AVC.h" #include "Network.h" #define RTP_VERSION 2 #define RTP_H264 96 static UDPContext *gUdpContext; int initRTPMuxContext(RTPMuxContext *ctx){ ctx->seq = 0; ctx->timestamp = 0; ctx->ssrc = 0x12345678; // 同源标志,可以设置随机数 ctx->aggregation = 1; // 当NALU长度小于指定长度时,是否采用聚合分组进行打包,否则使用单NALU分组方式打包 ctx->buf_ptr = ctx->buf; // buf存放除RTP Header的内容 ctx->payload_type = 0; // 当前版本只支持H.264 return 0; } // enc RTP packet void rtpSendData(RTPMuxContext *ctx, const uint8_t *buf, int len, int mark) { int res = 0; /* build the RTP header */ /* * * 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 | * : .... : * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * **/ uint8_t *pos = ctx->cache; pos[0] = (RTP_VERSION << 6) & 0xff; // V P X CC pos[1] = (uint8_t)((RTP_H264 & 0x7f) | ((mark & 0x01) << 7)); // M PayloadType Load16(&pos[2], (uint16_t)ctx->seq); // Sequence number Load32(&pos[4], ctx->timestamp); Load32(&pos[8], ctx->ssrc); // 复制RTP Payload memcpy(&pos[12], buf, len); // UDP socket发送 res = udpSend(gUdpContext, ctx->cache, (uint32_t)(len + 12)); printf("\nrtpSendData cache [%d]: ", res); for (int i = 0; i < 20; ++i) { printf("%.2X ", ctx->cache[i]); } printf("\n"); memset(ctx->cache, 0, RTP_PAYLOAD_MAX+10); ctx->buf_ptr = ctx->buf; // buf_ptr为buf的游标指针 ctx->seq = (ctx->seq + 1) & 0xffff; // RTP序列号递增 } // 拼接NAL头部 在 ctx->buff, 然后调用ff_rtp_send_data static void rtpSendNAL(RTPMuxContext *ctx, const uint8_t *nal, int size, int last){ printf("rtpSendNAL len = %d M=%d\n", size, last); // Single NAL Packet or Aggregation Packets if (size <= RTP_PAYLOAD_MAX){ // 采用聚合分组 if (ctx->aggregation){ /* * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR & Data | NALU 2 Size | NALU 2 HDR & Data | ... | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * * */ int buffered_size = (int)(ctx->buf_ptr - ctx->buf); // size of data in ctx->buf uint8_t curNRI = (uint8_t)(nal[0] & 0x60); // NAL NRI // The remaining space in ctx->buf is less than the required space if (buffered_size + 2 + size > RTP_PAYLOAD_MAX) { rtpSendData(ctx, ctx->buf, buffered_size, 0); buffered_size = 0; } /* * STAP-A/AP NAL Header * +---------------+ * |0|1|2|3|4|5|6|7| * +-+-+-+-+-+-+-+-+ * |F|NRI| Type | * +---------------+ * */ if (buffered_size == 0){ *ctx->buf_ptr++ = (uint8_t)(24 | curNRI); // 0x18 } else { // 设置STAP-A NAL HDR uint8_t lastNRI = (uint8_t)(ctx->buf[0] & 0x60); if (curNRI > lastNRI){ // if curNRI > lastNRI, use new curNRI ctx->buf[0] = (uint8_t)((ctx->buf[0] & 0x9F) | curNRI); } } // set STAP-A/AP NAL Header F = 1, if this NAL F is 1. ctx->buf[0] |= (nal[0] & 0x80); // NALU Size + NALU Header + NALU Data Load16(ctx->buf_ptr, (uint16_t)size); // NAL size ctx->buf_ptr += 2; memcpy(ctx->buf_ptr, nal, size); // NALU Header & Data ctx->buf_ptr += size; // meet last NAL, send all buf if (last == 1){ rtpSendData(ctx, ctx->buf, (int)(ctx->buf_ptr - ctx->buf), 1); } } // 采用单NALU分组 else { /* * 0 1 2 3 4 5 6 7 8 9 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |F|NRI| Type | a single NAL unit ... | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * */ rtpSendData(ctx, nal, size, last); } } else { // 分片分组 /* * * 0 1 2 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | FU indicator | FU header | FU payload ... | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * * */ if (ctx->buf_ptr > ctx->buf){ rtpSendData(ctx, ctx->buf, (int)(ctx->buf_ptr - ctx->buf), 0); } int headerSize; uint8_t *buff = ctx->buf; uint8_t type = nal[0] & 0x1F; uint8_t nri = nal[0] & 0x60; /* * FU Indicator * 0 1 2 3 4 5 6 7 * +-+-+-+-+-+-+-+-+ * |F|NRI| Type | * +---------------+ * */ buff[0] = 28; // FU Indicator; FU-A Type = 28 buff[0] |= nri; /* * FU Header * 0 1 2 3 4 5 6 7 * +-+-+-+-+-+-+-+-+ * |S|E|R| Type | * +---------------+ * */ buff[1] = type; // FU Header uses NALU Header buff[1] |= 1 << 7; // S(tart) = 1 headerSize = 2; size -= 1; nal += 1; while (size + headerSize > RTP_PAYLOAD_MAX) { // 发送分片分组除去首尾的中间的分片 memcpy(&buff[headerSize], nal, (size_t)(RTP_PAYLOAD_MAX - headerSize)); rtpSendData(ctx, buff, RTP_PAYLOAD_MAX, 0); nal += RTP_PAYLOAD_MAX - headerSize; size -= RTP_PAYLOAD_MAX - headerSize; buff[1] &= 0x7f; // buff[1] & 0111111, S(tart) = 0 } buff[1] |= 0x40; // buff[1] | 01000000, E(nd) = 1 memcpy(&buff[headerSize], nal, size); rtpSendData(ctx, buff, size + headerSize, last); } } // 从一段H264流中,查询完整的NAL发送,直到发送完此流中的所有NAL void rtpSendH264HEVC(RTPMuxContext *ctx, UDPContext *udp, const uint8_t *buf, int size){ const uint8_t *r; const uint8_t *end = buf + size; gUdpContext = udp; printf("\nrtpSendH264HEVC start\n"); if (NULL == ctx || NULL == udp || NULL == buf || size <= 0){ printf("rtpSendH264HEVC param error.\n"); return; } r = ff_avc_find_startcode(buf, end); while (r < end){ const uint8_t *r1; while (!*(r++)); // skip current startcode r1 = ff_avc_find_startcode(r, end); // find next startcode // send a NALU (except NALU startcode), r1==end indicates this is the last NALU rtpSendNAL(ctx, r, (int)(r1-r), r1==end); // control transmission speed usleep(1000000/25); // suppose the frame rate is 25 fps ctx->timestamp += (90000.0/25); r = r1; } }
AVC.h
#ifndef RTPSERVER_AVC_H #define RTPSERVER_AVC_H #include <stdint.h> /* copy from FFmpeg libavformat/acv.c */ const uint8_t *ff_avc_find_startcode(const uint8_t *p, const uint8_t *end); #endif //RTPSERVER_AVC_H
AVC.c
#include <stdio.h> #include "AVC.h" // 查找NALU起始码,直接copy的ffpmpeg代码 static const uint8_t *ff_avc_find_startcode_internal(const uint8_t *p, const uint8_t *end) { const uint8_t *a = p + 4 - ((intptr_t)p & 3); // a=p后面第一个地址为00的位置上 for (end -= 3; p < a && p < end; p++) { // 可能是保持4字节 对齐 if (p[0] == 0 && p[1] == 0 && p[2] == 1) return p; } for (end -= 3; p < end; p += 4) { uint32_t x = *(const uint32_t*)p; // 取4个字节 if ((x - 0x01010101) & (~x) & 0x80808080) { // X中至少有一个字节为0 if (p[1] == 0) { if (p[0] == 0 && p[2] == 1) // 0 0 1 x return p; if (p[2] == 0 && p[3] == 1) // x 0 0 1 return p+1; } if (p[3] == 0) { if (p[2] == 0 && p[4] == 1) // x x 0 0 1 return p+2; if (p[4] == 0 && p[5] == 1) // x x x 0 0 1 return p+3; } } } for (end += 3; p < end; p++) { // if (p[0] == 0 && p[1] == 0 && p[2] == 1) return p; } return end + 3; // no start code in [p, end], return end. } const uint8_t *ff_avc_find_startcode(const uint8_t *p, const uint8_t *end){ const uint8_t *out= ff_avc_find_startcode_internal(p, end); if(p < out && out < end && !out[-1]) out--; // find 0001 in x001 return out;
Network.h
#ifndef RTPSERVER_NETWORK_H #define RTPSERVER_NETWORK_H #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> typedef struct{ const char *dstIp; int dstPort; struct sockaddr_in servAddr; int socket; }UDPContext; /* create UDP socket */ int udpInit(UDPContext *udp); /* send UDP packet */ int udpSend(const UDPContext *udp, const uint8_t *data, uint32_t len); #endif //RTPSERVER_NETWORK_H
Network.c
// // Created by Liming Shao on 2018/5/11. // #include <stdio.h> #include <string.h> #include "Network.h" int udpInit(UDPContext *udp) { if (NULL == udp || NULL == udp->dstIp || 0 == udp->dstPort){ printf("udpInit error.\n"); return -1; } udp->socket = socket(AF_INET, SOCK_DGRAM, 0); if (udp->socket < 0){ printf("udpInit socket error.\n"); return -1; } udp->servAddr.sin_family = AF_INET; udp->servAddr.sin_port = htons(udp->dstPort); inet_aton(udp->dstIp, &udp->servAddr.sin_addr); // 先发个空字符测试能否发送UDP包 int num = (int)sendto(udp->socket, "", 1, 0, (struct sockaddr *)&udp->servAddr, sizeof(udp->servAddr)); if (num != 1){ printf("udpInit sendto test err. %d", num); return -1; } return 0; } int udpSend(const UDPContext *udp, const uint8_t *data, uint32_t len) { ssize_t num = sendto(udp->socket, data, len, 0, (struct sockaddr *)&udp->servAddr, sizeof(udp->servAddr)); if (num != len){ printf("%s sendto err. %d %d\n", __FUNCTION__, (uint32_t)num, len); return -1; } return len; }
Utils.h
#ifndef RTPSERVER_UTILS_H #define RTPSERVER_UTILS_H #include <stdint.h> uint8_t* Load8(uint8_t *p, uint8_t x); uint8_t* Load16(uint8_t *p, uint16_t x); uint8_t* Load32(uint8_t *p, uint32_t x); /* read a complete file */ int readFile(uint8_t **stream, int *len, const char *file); void dumpHex(const uint8_t *ptr, int len); #endif //RTPSERVER_UTILS_H
Utils.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include "Utils.h" uint8_t* Load8(uint8_t *p, uint8_t x) { *p = x; return p+1; } uint8_t* Load16(uint8_t *p, uint16_t x) { p = Load8(p, (uint8_t)(x >> 8)); p = Load8(p, (uint8_t)x); return p; } uint8_t* Load32(uint8_t *p, uint32_t x) { p = Load16(p, (uint16_t)(x >> 16)); p = Load16(p, (uint16_t)x); return p; } int readFile(uint8_t **stream, int *len, const char *file) { FILE *fp = NULL; long size = 0; uint8_t *buf; printf("readFile %s\n", file); fp = fopen(file, "r"); if (!fp) return -1; // 下面是获取文件大小的两种方式 #if 0 // C语言方式,Windows可以使用此方式 fseek(fp, 0L, SEEK_END); size = ftell(fp); fseek(fp, 0L, SEEK_SET); #else // Linux系统调用,不用读取全部文件内容,速度快 struct stat info = {0}; stat(file, &info); size = info.st_size; #endif buf = (uint8_t *)(malloc(size * sizeof(uint8_t))); memset(buf, 0, (size_t)size); if (fread(buf, 1, size, fp) != size){ printf("read err.\n"); return -1; } fclose(fp); *stream = buf; *len = (int)size; printf("File Size = %d Bytes\n", *len); return 0; } void dumpHex(const uint8_t *ptr, int len) { printf("%p [%d]: ", (void*)ptr, len); for (int i = 0; i < len; ++i) { printf("%.2X ", ptr[i]); } printf("\n"); }
RTP码流播放方法/SDP文件
本程序只实现了发送RTP视频流的服务器端功能,可以使用第三方软件ffmpeg-ffplay/VLC进行播放。播放RTP流需要一个写有视频流信息的SDP文件(play.sdp),此程序使用的文件如下所示。
m=video 1234 RTP/AVP 96 a=rtpmap:96 H264/90000 a=framerate:25 c=IN IP4 127.0.0.1 s=Sample Video
VLC播放
使用VLC先打开此sdp文件,然后运行此服务端程序。
FFplay
ffplay是ffmpeg中独立的播放器程序。可以使用如下命令就行播放,同样是先执行播放命令,后运行RTP发送程序。
ffplay -protocol_whitelist "file,rtp,udp" play.sdp
附ffmpeg RTP发送命令:ffmpeg -re -i Sample.h264 -vcodec copy -f rtp rtp://127.0.0.1:1234
关于RTP时间戳问题
RTP协议要求时间戳应该使用90kHz的采样时钟,也就是说一秒钟的间隔应该设置时间差值为90000,25pfs恒定帧率的视频每一帧时间戳就为900000/25。这是对于视频文件而言的,对于实时采集的视频流,可以使用视频采集时刻作为时间戳。
因为本例使用的是.h264裸流文件,文件格式本身并没有时间戳信息,所以本例中可以不设置时间戳信息,也可以根据帧率设置时间戳信息,通过分析网络数据包发现FFmpeg RTP发送.h264视频时时间戳采用的是一个固定的随机数,并没有逐帧递增。
但是不设置时间戳信息的话,就会影响客户端解码播放。ffplay播放RTP流的时候,在没有RTP时间戳的情况下会根据接收的速度进行解码显示,VLC在没有RTP时间戳时,会先缓存一段时间的视频流,然后正常播放,可能是通过分析NALU视频流获取了显示时间信息。