之前用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(t<k&&0!=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->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,歡迎下載查看。