本文是《國標GB28181協定裝置端開發》系列的第四篇,介紹了實時視訊資料傳輸的過程。通過解讀INVITE報文中的SDP資訊,讀取和解析視訊檔或圖片檔案,進行資料編碼,以及h264封裝為PS格式,最終通過RTP資料傳送,實現了GB28181協定裝置端的視訊傳輸功能。本文將逐步詳細介紹每個模組的實現步驟和相關技術要點,幫助讀者理解和應用GB28181協定進行實時視訊傳輸。
在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=
v=0
表示SDP協定版本號,此處為0。
o=34020000002000000001 0 0 IN 192.168.1.10
o欄位標識了對談的發起者和對談的唯一標識。
"34020000002000000001" 表示該對談對談發起者的SIP ID。
0 0 表示對談的起始和結束時間戳。
IN IP4 192.168.1.10 表示對談的網路地址,這裡為IPv4地址。
s欄位為對談的名稱或描述,此處為"Play"表面是實時音視訊
c=IN IP4 192.168.1.10
c欄位指定了對談的連線資訊。
IN 表示網路型別為Internet。
IP4 192.168.1.10 表示對談的IPv4地址。
t=0 0
t欄位指定了對談的時間資訊。
0 0 表示對談的起始和結束時間都為0,即持續時間未定義。
m=video 40052 RTP/AVP 96
m欄位定義了對談中的媒體型別和相關引數。
video 表示媒體型別為視訊。
40052 表示媒體流的傳輸埠號。
RTP/AVP 表示傳輸協定為RTP,使用AVP(Audio-Visual Profile)設定。
96 表示媒體流使用編號96表示。
a=rtpmap:96 PS/90000
a欄位包含了媒體流的屬性。
rtpmap:96 表示將編號為96的負載型別。
PS 表示使用MPEG-PS格式進行資料封裝。
90000 表示時鐘速率,即每秒的時鐘滴答數。
y=0358902090
y欄位為十進位制整數位符串,表示SSRC值
f=
f欄位:f= v/編碼格式/解析度/影格率/位元速率型別/位元速率大小a/編碼格式/位元速率大小/取樣率
這裡並沒有設定f欄位,由資料傳送端來填充
為了進行視訊資料傳輸,我們首先需要讀取和解析視訊檔或圖片檔案。我們需要使用相應的庫或工具,從檔案中讀取視訊或圖片資料,並進行解析,以獲取關鍵的視訊幀或影象資料,為後續的編碼和封裝做準備。
在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封裝的演示程式碼(僅展示部分程式碼):
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),轉載請註明作者和出處