H264 推流到RTMP服務器

參考連接:
基於libRTMP的流媒體直播之 AAC、H264 推送:
http://billhoo.blog.51cto.com/2337751/1557646/
使用librtmp進行H264與AAC直播
http://www.codeman.net/2014/01/439.html
雷博CSDN博客

下面是我最近了解PUST H264流到RTMP服務器上的一些筆記,參考了上面的鏈接,看別人的不如自己動手實踐一下,以下便是實驗過程記錄。

一、FLV結構簡析

關於FLV的結構,雷博的博文已經詳細說明了,在這裏可以參考一張圖片
這裏寫圖片描述
由上面的圖片可以看出,FLV的整體結構爲Header + Body. 其中Body則由 Tag + PreviousTagSizeN的方式組成,每一個Tag由Tag Header + Tag Data組成。
其中,在Video_File_Format_Specification_V10.pdf中,對每一個TAG的Tag Data有了更詳細的說明(暫時只關注視頻tag):
這裏寫圖片描述
這裏寫圖片描述

關於上面對Tag Data結構的說明,以前在做本地FLV視頻PUSH到RTMP的測試中,並沒有關注,只是直接將Tag Data封包進RTMP Packet裏面發送出去即可。

二、H264結構簡析

H264的碼流結構主要由SPS、 PPS、 IDR 幀(包含一個或多個 I-Slice)、 P 幀(包含一個或多個P-Slice)、 B 幀(包含一個或多個 B-Slice)等部分組成。

關於SPS和PPS,引用以下:

  • 將一個視頻序列 (從 IDR 幀開始到下一個 IDR 幀之前的數據稱爲一個視頻序列) 全部圖像的共同特徵抽取出來,放在 SPS 語法單元中。
  • 將各個圖像的典型特徵抽取出來,放在 PPS 語法單元中。
  • 只有視頻序列之間才能切換 SPS,即只有 IDR 幀的第一個 slice 可以切換 SPS。
  • 只有圖像之間才能切換 PPS,即只有每幀圖像的第一個 slice 才能切換 PPS

H.264 的所有語法結構最終都被封裝成 nalu。碼流中的 nalu單元必須定義合適的分隔符,否則無法區分,因此採用前綴碼“00 00 01”或者“00 00 00 01”來區分每一個nalu單元。在每個前綴碼後面緊跟的一個字節爲nalu的語法結構,由三部分組成forbidden_bit(1bit),nal_reference_bit(2bits)(優先級),nal_unit_type(5bits)(類型)。
forbidden_bit:禁止位。
nal_reference_bit:當前NAL的優先級,值越大,該NAL越重要。
nal_unit_type :NAL類型。
其中,我們着重需要關注的是nal_unit_type ,因爲它代表了這個NALU的具體荷載數據的類型,我們可以通過 nal_unit_type & 0x1f的方式來獲取其類型,具體定義如下:
這裏寫圖片描述

三、H264流的封裝

在PUSH FLV到RTMP服務器的時候,我們只需將FLV 的Tag Data封裝進RTMP Packet,然後調用RTMP_SendPacket()函數將數據發送出去。
因此,在將H264 PUSH給RTMP服務器的時候,我們也需要按照FLV的格式將H264數據進行封包。因此我們需要構造視頻Tag,並且我們在發送第一包數據前,需要構造“AVC Sequence Header”,用於告訴RTMP服務器解碼相關的信息。
再次呈上上面的兩幅圖:
這裏寫圖片描述
這裏寫圖片描述

上面兩幅圖爲FLV 每一個TAG 中的Tag Data裏面的組成,因此我們需要根據上面的結構構造視頻同步包 和 H264碼流包

1)視頻同步包的構造[AVC Sequence Header]

視頻同步包的構造,其實是需要我們將H264中的SPS PPS按照一定的格式發給RTMP服務器,供RTMP服務器解碼器進行解碼。根據上面的圖片可知,當構造視頻同步包的時候,各個結構如下:

VIDEODATA
FrameType == 1
CodecID == 7
VideoData == AVCVIDEOPACKET

AVCVIDEOPACKET
AVCPacketType == 0x00
CompositionTime == 0x000000
Data == AVCDecoderConfigurationRecord

接下來繼續構造AVCDecoderConfigurationRecord,參考上面鏈接,AVCDecoderConfigurationRecord相關結構如下:

這裏寫圖片描述

因此我們需要將H264中的SPS PPS信息按照上面的結構進行填充,代碼如下:

int send_sps_pps(unsigned char* index_body,int index_body_len)
{
    unsigned char *sps,*pps;
    int sps_temp,pps_temp;
    int sps_len,pps_len;
    int i,temp;
    unsigned char body[1024];
    memset(body,0,1024);

    /* find *sps *pps */
    temp = find_sps_pps(index_body,index_body_len,&sps_temp,&pps_temp,&sps_len,&pps_len);
    if(temp < 0)
        return -1;
    sps = index_body + sps_temp;
    pps = index_body + pps_temp;

    /*去掉幀界定符*/ 
    sps += 4;
    pps += 4;
    sps_len -= 4;
    pps_len -= 4;

    /*AVC head*/
    i = 0;
    body[i++] = 0x17;
    body[i++] = 0x00;
    body[i++] = 0x00;
    body[i++] = 0x00;
    body[i++] = 0x00;

    /*AVCDecoderConfigurationRecord*/
    body[i++] = 0x01;
    body[i++] = *(sps+1);
    body[i++] = *(sps+2);
    body[i++] = *(sps+3);
    body[i++] = 0xff;

    /*sps*/
    body[i++]   = 0xe1;
    body[i++] = (sps_len >> 8) & 0xff;
    body[i++] = sps_len & 0xff;
    memcpy(&body[i],sps,sps_len);
    i +=  sps_len;

    /*pps*/
    body[i++]   = 0x01;
    body[i++] = (pps_len >> 8) & 0xff;
    body[i++] = (pps_len) & 0xff;
    memcpy(&body[i],pps,pps_len);
    i +=  pps_len;

    /*send rtmp packet*/
    if(rtmp_packet_send(body,i,RTMP_PACKET_TYPE_VIDEO,0) < 0)
        return -2;
    return temp;
}

2)H264普通數據包的構建

構建了上面的視頻同步包後,我們需要將H264的荷載的數據即H264的I P B等數據封包進去。根據上面的圖片,結構信息如下:

VIDEODATA
FrameType ==(I frame ? 1 :2)
CodecID == 7
VideoData == AVCVIDEOPACKET

AVCVIDEOPACKET
AVCPacketType == 0x01
CompositionTime == 0x000000
Data == NALU

上面的Data == H264 NALU Size + NALU Raw Data

代碼如下:

int send_rtmp_video(unsigned char* index_body,int index_body_len,unsigned int timestamp)
{
    unsigned char *body;
    int type;

    /*去掉幀界定符*/  
    if (index_body[2] == 0x00) { /*00 00 00 01*/  
            index_body += 4;  
            index_body_len -= 4;  
    } else if (index_body[2] == 0x01){ /*00 00 01*/  
            index_body += 3;  
            index_body_len -= 3;  
    }  

    /*申請發送數據空間*/
    body = (unsigned char*)malloc(index_body_len+9); 

     /*send video packet*/   
    memset(body,0,index_body_len+9);  

    /*key frame*/  
    body[0] = 0x27;  
    type = index_body[0]&0x1f;  
    if (type == NAL_SLICE_IDR)   
      body[0] = 0x17;  

     /*nal unit*/  
    body[1] = 0x01;  
    body[2] = 0x00;  
    body[3] = 0x00;  
    body[4] = 0x00;  

    //data length
    body[5] = (index_body_len >> 24) & 0xff;  
    body[6] = (index_body_len >> 16) & 0xff;  
    body[7] = (index_body_len >>  8) & 0xff;  
    body[8] = (index_body_len ) & 0xff;  

    /*copy data*/  
    memcpy(&body[9],index_body,index_body_len);  

    /*send rtmp packet*/
    if(rtmp_packet_send(body,index_body_len + 9,RTMP_PACKET_TYPE_VIDEO,timestamp) < 0)
        return -1;


    /*釋放發送數據空間*/
    free(body);

    return 0;

}

四、小結

在剛開始接觸H264 PUSH到RTMP時,一頭霧水,不理解如何進行操作,然後通過查閱資料,得到了初步的瞭解,將H264推流到RTMP服務器,關鍵的也就是視頻同步包的構造,和H264普通數據包的構造,以及在將這些數據封包進RTMP Packet時候,要注意時間戳之間的關係。除了這些,其他方面例如RTMP的初始化,建立連接等方面則和RTMPDUMP中的流程一致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章