1. 預備
視頻:
由一副副連續的圖像構成,由於數據量比較大,因此為了節省帶寬以及存儲,就需要進行必要的壓縮與解壓縮,也就是編解碼。
h264裸碼流:
對一個圖像或者一個視頻序列進行壓縮,即產生碼流,采用H264編碼后形成的碼流就是h264裸碼流。
碼流傳輸:
發送端將H264裸碼流打包后進行網絡傳輸,接收端接收后進行組包還原裸碼流,然后可以再進行存儲,轉發,或者播放等等相關的處理。
存儲轉發可以直接使用裸碼流,播放則需要進行解碼和顯示處理
解碼顯示:
一般會解成YUV數據,然后交給顯示相關模塊做處理,如使用openGL或者D3D等進行渲染(采用3d的方式來顯示2d的圖像稱為渲染)
解碼又分軟件解碼和硬件解碼,
軟解一般ffmpeg
硬解則各不同,由各硬件廠商開放sdk來處理,如:hisi,intel media sdk,nvida gpu,apple ios videotoolbox等。
2. h264碼流傳輸
類似發送網頁文本數據有http一樣,視頻數據在網絡上傳輸也有專門的網絡協議來支持,如:rtsp,rtmp等
由於碼流是一幀一幀的圖片數據,所以傳輸的時候也是一幀幀來傳輸的,因此這里就會涉及到各種類型的幀處理了
I 幀: 參考幀或者關鍵幀,可以理解為是一幀完整畫面,解碼時只需要本幀數據就解碼成一幅完整的圖片,數據量比較大。
P幀: 差別幀,只有與前面的幀或I幀的差別數據,需要依賴前面已編碼圖象作為參考圖象,才能解碼成一幅完整的圖片,數據量小。
B幀: 雙向差別幀,也就是B幀記錄的是本幀與前后幀的差別,需要依賴前后幀和I幀才能解碼出一幅完整的圖片,數據量小。
由於i幀比較大,已經超出mtu最大1500,所以需要拆包分片傳輸,這里說的拆包發送不是指發送超過1500的數據包時tcp的分段傳輸或者upd的ip分片傳輸,而是指rtp協議本身對264的拆包。
rtp打包后將數據交給tcp或者upd進行傳輸的時候就已經控制在1500以內,這樣可以提高傳輸效率,避免一個I幀萬一丟失就會造成花屏或者重傳造成延時卡頓等等問題。
(順便提一句,rtmp打包就比較簡單,由於是基於tcp的協議,大包直接交給tcp去做分段傳輸,rtmp通過設置合適的trunk size去發送一幀幀數據)
既然要進行拆包發送與接收,就少不了需要相關的包結構以及打包組包了,繼續。。。。。。
3. H264在網絡傳輸的單元:NALU
NALU結構:NALU header(1byte) + NALU payload
header 部分可以判斷類型等信息,從右往左5個bit位
SPS: 0x67 header & 0x1F = 7 | I Frame: 0x65 header & 0x1F = 5 |
PPS: 0x68 header & 0x1F = 8 | P Frame: 0x41 header & 0x1F = 1 |
SEI: 0x66 header & 0x1F = 6 |
payload 部分可以簡單理解為 編碼后的264幀數據
詳細可以去查閱 h264 NALU 語法結構
4. h264的RTP打包
1. 單NALU: P幀或者B幀比較小的包,直接將NALU打包成RTP包進行傳輸 RTP header(12bytes) + NALU header (1byte) + NALU payload
2. 多NALU: 特別小的包幾個NALU放在一個RTP包中
3. FUs(Fragment Units): I幀長度超過MTU的,就必須要拆包組成RTP包了,有FU-A,FU-B
RTP header (12bytes)+ FU Indicator (1byte) + FU header(1 byte) + NALU payload
看到這里會不禁思考,
1. NALU頭不見了,如何判斷類型?實際上NALU頭被分散填充到FU indicator和FU header里面了
bit位按照從左到右編號0-7來算,nalu頭中0-2前三個bit放在FU indicator的0-2前三個bit中,后3-7五個bit放入FU header的后3-7五個中
//NALU header = (FU indicator & 0xe0) | (FU header & 0x1F) 取FU indicator前三和FU header后五
headerStart[1] = (headerStart[0]&0xE0)|(headerStart[1]&0x1F);
因此查看I幀p幀類型,遇到FU分片的,直接看第二個字節,即Fu header后五位,這個跟直接看NALU頭並無差異,一般有:0x85,0x05,0x45等等
2. 多個RTP包如何還原組合成回一個完整的I幀? 在FU header中有標記為判斷
照舊從左到右,Fu header前兩個bit表示start和end標記,start為1表示一個i幀分片開始,end為1表示一個i幀分片結束
3. 如何查看是一個I幀分片開始?
看第一個字節FU Indicator,照舊從左到右,Fu Indicator前三個bit是NALU頭的前三個bit,后五位為類型FU-A:28(11100),FU-B:29(11101)
RTP抓包看下來整個字節是0x7c開頭
如果不是分片,第一字節就是NALU頭,如:0x67,0x68,0x41等
5. 抓包分析如下圖:
--->RTP包中接收的264包是不含有0x00,0x00,0x00,0x01頭的,這部分是rtp接收以后,另外再加上去的,解碼的時候再做判斷的。
1. SPS
2. I幀分片開始,第一個字節FU Indicator,0x7c, 后五位11100,28,FU-A
第二個字節FU Header,0x85,前兩個bit start位1,end位0 表示 分片開始,后五個bit值5,I幀
I幀分片,0x7c開頭,第二個字節0x05, FU Header start和end位 0,后五個bit值5,I幀
I幀分片結束,7c開頭,第二個字節0x45,FU Headr,start 0,end1,后五個bit值5,I幀, Mark標記一幀結束
3. p幀,第一個字節:0x41
4. P幀分片開始:第一個字節FU Indicator,0x7c, 后五位11100,28,FU-A
第二個字節FU Header,0x81, 前兩個bit start位1,end位0 表示 分片開始,后五個bit 值是1,p幀
P幀分片結束:0x5c開頭,第二個字節0x41,FU Headr,start 0,end1,后五個bit值1,P幀, Mark標記一幀結束
6. 參考live555相關的源碼如下:
Boolean H264VideoRTPSource ::processSpecialHeader(BufferedPacket* packet, unsigned& resultSpecialHeaderSize) { unsigned char* headerStart = packet->data(); unsigned packetSize = packet->dataSize(); unsigned numBytesToSkip; // Check the 'nal_unit_type' for special 'aggregation' or 'fragmentation' packets: if (packetSize < 1) return False; fCurPacketNALUnitType = (headerStart[0]&0x1F); //FU Indicator后五位即NALU類型 0x1F = 0001 1111 switch (fCurPacketNALUnitType) { case 24: { // STAP-A numBytesToSkip = 1; // discard the type byte break; } case 25: case 26: case 27: { // STAP-B, MTAP16, or MTAP24 numBytesToSkip = 3; // discard the type byte, and the initial DON break; } case 28: case 29: { // // FU-A or FU-B // For these NALUs, the first two bytes are the FU indicator and the FU header. // If the start bit is set, we reconstruct the original NAL header into byte 1: if (packetSize < 2) return False; unsigned char startBit = headerStart[1]&0x80; //FU Header start標記位 0x80= 1000 0000 unsigned char endBit = headerStart[1]&0x40; //FU Header End標記位 0x40= 0100 0000 if (startBit) { fCurrentPacketBeginsFrame = True; headerStart[1] = (headerStart[0]&0xE0)|(headerStart[1]&0x1F); //還原NALU頭 numBytesToSkip = 1; } else { // The start bit is not set, so we skip both the FU indicator and header: fCurrentPacketBeginsFrame = False; numBytesToSkip = 2; } fCurrentPacketCompletesFrame = (endBit != 0); break; } default: { // This packet contains one complete NAL unit: fCurrentPacketBeginsFrame = fCurrentPacketCompletesFrame = True; //默認沒有分片,完整的NALU numBytesToSkip = 0; break; } } resultSpecialHeaderSize = numBytesToSkip; return True; }