國標GB28181協定使用者端開發(四)實時視訊資料傳輸

2023-07-06 12:00:50

國標GB28181協定使用者端開發(四)實時視訊資料傳輸

本文是《國標GB28181協定裝置端開發》系列的第四篇,介紹了實時視訊資料傳輸的過程。通過解讀INVITE報文中的SDP資訊,讀取和解析視訊檔或圖片檔案,進行資料編碼,以及h264封裝為PS格式,最終通過RTP資料傳送,實現了GB28181協定裝置端的視訊傳輸功能。本文將逐步詳細介紹每個模組的實現步驟和相關技術要點,幫助讀者理解和應用GB28181協定進行實時視訊傳輸。

一、INVITE報文的SDP資訊解讀

在GB28181協定中,在實時音視訊傳輸過程中,使用INVITE報文攜帶SDP(Session Description Protocol)資訊。SDP資訊描述了對談的屬性和引數,包括媒體型別、傳輸協定、編解碼器、網路地址等。下面是一個範例INVITE報文的SDP內容,並對其中的每一項進行詳細解釋:

v=0
o=34020000002000000001 0 0 IN IP4 192.168.1.10
s=Play
c=IN IP4 192.168.1.10
t=0 0
m=video 40052 RTP/AVP 96
a=recvonly
a=rtpmap:96 PS/90000
y=0358902090
f=
  1. v=0

    表示SDP協定版本號,此處為0。

  2. o=34020000002000000001 0 0 IN 192.168.1.10

    o欄位標識了對談的發起者和對談的唯一標識。
    "34020000002000000001" 表示該對談對談發起者的SIP ID。
    0 0 表示對談的起始和結束時間戳。
    IN IP4 192.168.1.10 表示對談的網路地址,這裡為IPv4地址。

  3. s欄位為對談的名稱或描述,此處為"Play"表面是實時音視訊

  4. c=IN IP4 192.168.1.10

    c欄位指定了對談的連線資訊。
    IN 表示網路型別為Internet。
    IP4 192.168.1.10 表示對談的IPv4地址。

  5. t=0 0

    t欄位指定了對談的時間資訊。
    0 0 表示對談的起始和結束時間都為0,即持續時間未定義。

  6. m=video 40052 RTP/AVP 96

    m欄位定義了對談中的媒體型別和相關引數。
    video 表示媒體型別為視訊。
    40052 表示媒體流的傳輸埠號。
    RTP/AVP 表示傳輸協定為RTP,使用AVP(Audio-Visual Profile)設定。
    96 表示媒體流使用編號96表示。

  7. a=rtpmap:96 PS/90000

    a欄位包含了媒體流的屬性。
    rtpmap:96 表示將編號為96的負載型別。
    PS 表示使用MPEG-PS格式進行資料封裝。
    90000 表示時鐘速率,即每秒的時鐘滴答數。

  8. y=0358902090

    y欄位為十進位制整數位符串,表示SSRC值

  9. f=

    f欄位:f= v/編碼格式/解析度/影格率/位元速率型別/位元速率大小a/編碼格式/位元速率大小/取樣率
    這裡並沒有設定f欄位,由資料傳送端來填充

二、視訊檔或圖片檔案的讀取、解析和編碼

為了進行視訊資料傳輸,我們首先需要讀取和解析視訊檔或圖片檔案。我們需要使用相應的庫或工具,從檔案中讀取視訊或圖片資料,並進行解析,以獲取關鍵的視訊幀或影象資料,為後續的編碼和封裝做準備。

三、h264封裝PS

在GB28181協定中,視訊資料通常以MPEG-PS(MPEG Program Stream)格式進行封裝。需要將經過編碼的視訊資料進行PS格式的封裝,包括新增包裝頭和起始碼,然後再進一步封裝RTP。

以下是使用C++將H.264的NALU封裝為MPEG-PS格式的主要過程(僅展示部分程式碼):

// 將H.264的NALU列表封裝為MPEG-PS格式
void MakeMPEGPS(unsigned char* h264Data, int h264Length,
    unsigned char* psData)
{
    int totalPES = (h264Length + MAX_PES_LENGTH - 1) / MAX_PES_LENGTH; // 計算總的PES包數
    int remainingBytes = h264Length; // 剩餘待處理的位元組數

    // MPEG-PS包頭
    unsigned char mpegPSHeader[] = {0x00, 0x00, 0x01, 0xBA};

    // 分割並封裝H.264資料
    for (int i = 0; i < totalPES; i++)
    {
        unsigned char* pbuf = psData;

        int pesLength = (remainingBytes > MAX_PES_LENGTH) ? MAX_PES_LENGTH : remainingBytes; // 當前PES包的長度
        remainingBytes -= pesLength; // 更新剩餘待處理的位元組數

        // PES包頭
        unsigned char pesHeader[] = {0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00};

        // 設定PES包長度
        pesHeader[4] = (pesLength + 8) >> 8; // 高8位元
        pesHeader[5] = (pesLength + 8) & 0xFF; // 低8位元

        // 輸出MPEG-PS包頭和當前PES包頭
        memcpy(pbuf, mpegPSHeader, sizeof(mpegPSHeader));
        pbuf += sizeof(mpegPSHeader);

        memcpy(pbuf, pesHeader, sizeof(pesHeader));
        pbuf += sizeof(pesHeader);

        // 輸出當前PES包的H.264資料
        memcpy(pbuf, h264Data + (i * MAX_PES_LENGTH), pesLength);
        pbuf += pesLength;

        int payload_len = (pbuf - psData);

        // 封裝RTP包並行送
        MakeAndSendRTP(psData, payload_len);
    }
}

需要注意到,當h264幀比較大的時候,會超出PES可表述的長度大小,這個時候必須對h264幀進行切分,封裝成多個PES,再合成到PS包中。

四、RTP資料傳送

RTP資料傳送的邏輯比較簡單,以下為程式中的程式碼示意圖

以下為RTP封裝的演示程式碼(僅展示部分程式碼):

struct RTPHeader
{
    uint8_t version; // RTP協定版本號,固定為2
    uint8_t padding: 1; // 填充位
    uint8_t extension: 1; // 擴充套件位
    uint8_t csrcCount: 4; // CSRC計數器,指示CSRC識別符號的個數
    uint8_t marker: 1; // 標記位
    uint8_t payloadType: 7; // 負載型別
    uint16_t sequenceNumber; // 序列號
    uint32_t timestamp; // 時間戳
    uint32_t ssrc; // 同步信源識別符號
};

void MakeRTPHeader(struct RTPHeader* header, uint16_t sequenceNumber, uint32_t timestamp, uint32_t ssrc, bool isMark)
{
    // 設定RTP協定版本號為2
    header->version = 2;
    // 填充位、擴充套件位、CSRC計數器等欄位根據具體需求進行設定
    header->padding = 0;
    header->extension = 0;
    header->csrcCount = 0;
    // 設定標記位為0(如果需要設定為1,則在需要設定的地方進行修改)
    header->marker = isMark ? 1 : 0;
    // 設定負載型別(payload type),根據具體需求進行設定
    header->payloadType = 96;
    // 設定序列號和時間戳
    header->sequenceNumber = htons(sequenceNumber); // 需要進行位元組序轉換(網路位元組序)
    header->timestamp = htonl(timestamp); // 需要進行位元組序轉換(網路位元組序)

    // 設定同步信源識別符號
    header->ssrc = htonl(ssrc); // 需要進行位元組序轉換(網路位元組序)
}

void sendRTPPacket(const uint8_t* mpegPSData, int mpegPSLength, uint16_t sequenceNumber, uint32_t timestamp, uint32_t ssrc)
{
    int offset = 0; // 偏移量,用於遍歷MPEG-PS包資料
    int remainingLength = mpegPSLength; // 剩餘長度,用於判斷是否需要分割RTP報文
    uint8_t rtpbuf[RTP_PAYLOAD_MAX_SIZE]; // RTP負載資料緩衝區
    struct RTPHeader rtpHeader; // RTP報文頭部

    while (remainingLength > 0)
    {
        // 計算當前RTP負載資料長度(不超過RTP負載最大大小)
        bool is_mark = false;
        int data_len = RTP_PAYLOAD_MAX_SIZE;
        if (remainingLength <= RTP_PAYLOAD_MAX_SIZE)
        {
            data_len = remainingLength;
            is_mark = true;
        }

        // 填寫RTP報文頭部
        MakeRTPHeader(&rtpHeader, sequenceNumber, timestamp, ssrc);

        // 複製RTP頭部到RTP負載緩衝區
        memcpy(rtpbuf, &rtpHeader, sizeof(RTPHeader));

        // 複製MPEG-PS資料到RTP負載緩衝區
        memcpy(rtpbuf + RTP_HEADER_LEN, mpegPSData + offset, data_len);

        // 將完整RTP包傳送出去
        if (udp_channel_)
        {
            udp_channel_->PostSendBuf(rtpbuf, RTP_HEADER_LEN + data_len);
        }

        // 更新偏移量、剩餘長度、序列號等資訊
        offset += data_len;
        remainingLength -= data_len;
        sequenceNumber++;
    }
}


合作請加WX:hbstream
合作請加作者hbstream(http://haibindev.cnblogs.com),轉載請註明作者和出處