之前用Jrtp的库来传输H264视频时,将摄像头编码后的视频数据直接发送,然后在另外一个开放板接收数据,解码,显示,实现效果很不错。一个开发板编码发送,一个开发板接收解码,不用考虑帧率,也不用考虑RTP数据报头部数据各个位的含义。然而想做到在开发板上采集,电脑上通过VLC播放时,却一直实现不了。后来在网上找了个通过UDP实现RTP协议的代码,终于OK了。通过WireShark抓包发现,我在用Jrtp传输H264视频时,数据报负载部分(Payload)是没啥问题,可数据报的头部那12个字节确有点莫名其妙,与直接用UDP socket函数发送的抓包数据差异很大,这个要解决,估计也只能查查Jrtplib的源码了,看看哪里没配置对。看Jrtplib的网站,自2011年后就没有更新了,我也打算后续用更为强大的ffmpeg库来做实时视频传输,所以Jrtplib就暂停研究,后面我会写个用它做板子到板子之间视频传输的博客。这里就先写一写已经实现了通过RTP传输H264格式视频的过程,首先来介绍几个概念。
实时传输协议(RTP)是在Internet上处理多媒体数据流的一种网络协议,利用它能够在一对一或者一对多的网络环境中实现流媒体数据的实时传输。RTP通常使用UDP来进行多媒体数据的传输,但如果需要的话也可以使用TCP等其他协议,整个RTP协议由两个密切相关的部分组成:RTP数据协议和RTCP控制协议。
RTP数据协议负责对流媒体数据进行封包并实现媒体流的实时传输,每一个RTP数据报都由头部(Header)和负载(Payload)两个部分组成,其中头部前12个字节的含义是固定的,而负载则可以使音频或者视频数据。
RTCP控制协议需要与RTP数据协议一起配合使用,当应用程序启动一个RTP会话时将同时占用两个端口,分别供RTP和RTCP使用。RTP本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完成。通常RTCP会采用与RTP相同的分发机制,向会话中的所有成员周期性地发送控制信息,应用程序通过接收这些数据,从中获取会话参与者的相关资料,以及网络状况、分组丢失概率等反馈信息,从而能够对服务质量进行控制或者对网络状况进行诊断。
实时流协议RTSP,它的意义在于使得实时流媒体数据的受控和点播变得可能。总的来说,RTSP是一个流媒体表示协议,主要来控制具有实时特性的数据发送,但它本身并不传输数据,而是必须依赖于下层传输协议所提供的某些服务。RTSP可以对流媒体提供诸如播放、暂停、快进等操作,它负责定义具体的控制消息、操作方法、状态码等,此外还描述了与RTP间的交互操作。
接下来要做的就比较清楚了,只要实现RTP数据协议即可实现直播的效果。发送RTP数据报时,需要设置的为头部(Header)和负载(Payload)两部分,也就是“数据头+数据”这样的形式。先来看下Header。
这张图中,前12个字节在每个RTP包中都存在,他们是:V版版号(2bit),P填充位(1bit),X扩展位(1bit),CC是CSRC的计数位(4bits);M标记位(1bit),PT有效载荷的类型(7bits);sequence number(2Bytes);timestamp时间戳位(4Bytes);SSRC同步标识位(4Bytes); CSRC不是RTP必须的(4Bytes)。
这样的话,用一个结构体来存储RTP的Header数据,代码如下。
typedef struct
{
/**//* byte 0 */
unsigned char csrc_len:4; /**//* expect 0 */
unsigned char extension:1; /**//* expect 1, see RTP_OP below */
unsigned char padding:1; /**//* expect 0 */
unsigned char version:2; /**//* expect 2 */
/**//* byte 1 */
unsigned char payload:7; /**//* RTP_PAYLOAD_RTSP */
unsigned char marker:1; /**//* expect 1 */
/**//* bytes 2, 3 */
unsigned short seq_no;
/**//* bytes 4-7 */
unsigned long timestamp;
/**//* bytes 8-11 */
unsigned long ssrc; /**//* stream number is used here. */
} RTP_FIXED_HEADER;
具体数据该怎么设置,后面程序再说。接下来看RTP数据报的的负载(Payload)部分。H.264 Payload格式定义了三种不同的基本负载(Payload)结构,接收端可通过RTP Payload的第一个字节来识别它们,这一个字节类似NALU头的格式,结构如下。
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
F(1bit),在H.264规范中规定这一位必须为0;NRI(2bits),用于指示这个NALU的重要性;字段Type(5bits),指在RTP Payload中表示NAL单元的类型,它和NALU头对应字段的区别是,当type的值为24~31表示这是一个特别格式的NAL单元,而H.264中,只取1~23是有效值。三种不同的基本负载(Payload)结构如下:
1. 单一NAL单元模式
即一个RTP数据报中仅包含一个完整的NALU,这种情况下PRT NAL头type字段和原始的H.264的NALU头类型字段是一样的。
2. 组合封包模式
即可能由多个NAL单元组成一个RTP包,分别有4种组合模式:STAP-A,STAP-B,MTAP16,MTAP24,对应的type值分别为24,25,26以及27。
3. 分片封包模式
用于把一个NALU单元封装成多个RTP包,存在两种类型FU-A和FU-B,对应的type值分别为28和29。
那么在发送时如何来选择封包模式呢?S5PV210实时视频传输的过程为先采集一帧摄像头原始数据(YUV格式),拷贝至编码器输入缓冲区,启动编码,取得一帧H.264格式数据(一个NALU单元),拷贝至RTP发送缓冲区,然后发送即可,接收以及解码显示都是VLC端要做的工作。既然是取一帧发一帧,那么就不用考虑组合封包模式,直接根据NALU单元的长度选择是否分片封包即可。这里RTP采用UDP传输,由于UDP数据报长度超过1500字节时,会自动拆分发送,增大了丢包概率,那么去除UDP数据报头以及RTP的Header部分,这里设置Payload部分最大长度为1400字节即可,当NALU单元长度大于1400字节时,就用分片封包模式。
在单一NAL单元模式中,对于一个原始的 H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成,,其中 Start Code 用于标示这是一个NALU 单元的开始,,必须是 "00 00 00 01" 或 "00 00 01",NALU 头仅一个字节,其后都是 NALU 单元内容。打包时去除 "00 00 01" 或 "00 00 00 01" 的开始码,把其他数据封包的 RTP 包即可。
例如,一个H.264的NALU是这样的:
[00 00 00 01 67 42 A0 1E 23 56 0E 2F ... ]
这时一个序列参数集NAL单元。[00 00 00 01]是四个字节的开始码,67是NALU头,42开始的数据是NALU内容,封装成RTP包将如下:
[RTP HEADER] [67 42 A0 1E 23 56 0E 2F ...]
发送单一NAL单元的代码如下。
RTP_FIXED_HEADER *rtp_hdr;
rtp_hdr =(RTP_FIXED_HEADER*)&sendBuf[0];
rtp_hdr->payload = H264;
rtp_hdr->version = 2;
rtp_hdr->marker = 0;
rtp_hdr->ssrc = htonl(10);
if(nalu_t->len<=MAX_RTP_PKT_LENGTH){
rtp_hdr->marker=1;
rtp_hdr->seq_no = htons(seq_num ++);
nalu_payload = &sendBuf[12];
memcpy(nalu_payload, nalu_t->buf, nalu_t->len);
ts_current=ts_current+timestamp_increse;
rtp_hdr->timestamp = htonl(ts_current);
bytes = nalu_t->len + 12 ;
::send(socketFd, sendBuf, bytes, 0);
}
当一个NALU单元长度较长时,采用分片封包模式,也称为Fragmentation Units (FUs)。
分片封包模式下是由FU indicator和FU header两个字节替换了NALU的开始码[00 00 00 01]以及NALU header,其它都一样,下面程序是分片封包发送的程序。
RTP_FIXED_HEADER *rtp_hdr;
rtp_hdr =(RTP_FIXED_HEADER*)&sendBuf[0];
rtp_hdr->payload = H264;
rtp_hdr->version = 2;
rtp_hdr->marker = 0;
rtp_hdr->ssrc = htonl(10);
if(nalu_t->len>MAX_RTP_PKT_LENGTH){
int k=0,l=0;
k = nalu_t->len/MAX_RTP_PKT_LENGTH;
l = nalu_t->len%MAX_RTP_PKT_LENGTH;
int t=0;
ts_current = ts_current+timestamp_increse;
rtp_hdr->timestamp = htonl(ts_current);
while(t<=k){
rtp_hdr->seq_no = htons(seq_num ++);
if(!t){
rtp_hdr->marker=0;
fu_ind =(FU_INDICATOR*)&sendBuf[12];
fu_ind->F=nalu_t->forbidden_bit;
fu_ind->NRI=nalu_t->nal_reference_idc>>5;
fu_ind->TYPE=28;
fu_hdr =(FU_HEADER*)&sendBuf[13];
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=1;
fu_hdr->TYPE=nalu_t->nal_unit_type;
nalu_payload=&sendBuf[14];
memcpy(nalu_payload,nalu_t->buf+1,MAX_RTP_PKT_LENGTH);
bytes=MAX_RTP_PKT_LENGTH+14;
::send( socketFd, sendBuf, bytes, 0 );
t++;
}else if(k==t){
rtp_hdr->marker=1;
fu_ind =(FU_INDICATOR*)&sendBuf[12];
fu_ind->F=nalu_t->forbidden_bit;
fu_ind->NRI=nalu_t->nal_reference_idc>>5;
fu_ind->TYPE=28;
fu_hdr =(FU_HEADER*)&sendBuf[13];
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->TYPE=nalu_t->nal_unit_type;
fu_hdr->E=1;
nalu_payload=&sendBuf[14];
memcpy(nalu_payload,nalu_t->buf+t*MAX_RTP_PKT_LENGTH+1,l-1);
bytes=l-1+14;
::send( socketFd, sendBuf, bytes, 0 );
t++;
}else if(tmarker=0;
fu_ind =(FU_INDICATOR*)&sendBuf[12];
fu_ind->F=nalu_t->forbidden_bit;
fu_ind->NRI=nalu_t->nal_reference_idc>>5;
fu_ind->TYPE=28;
fu_hdr =(FU_HEADER*)&sendBuf[13];
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->E=0;
fu_hdr->TYPE=nalu_t->nal_unit_type;
nalu_payload=&sendBuf[14];
memcpy(nalu_payload,nalu_t->buf+t*MAX_RTP_PKT_LENGTH+1,MAX_RTP_PKT_LENGTH);
bytes=MAX_RTP_PKT_LENGTH+14;
::send( socketFd, sendBuf, bytes, 0 );
t++;
}
}
}
明确了前面的概念后,将RTP的发送函数封装到一个RTP类中,然后在编码后,调用RTP类中的发送函数将编码输出数据发送即可。要注意的是,电脑端VLC播放器需要设置一个SDP文件,来配置VLC要播放的流媒体参数,下面是SDP文件的内容。
m=video 1234 RTP/AVP 96
a=rtpmap:96 H264/90000
a=decode_buf=300
a=framerate:50
c=IN IP4 192.168.2.100
整个工程我上传到了http://download.csdn.net/detail/westlor/9409924,欢迎下载查看。