什么是 STAP-A?
STAP-A 是一種特殊的 RTP 封裝機制,專為 H.264 和 H.265 這類視頻編碼協議設計。它的核心目的只有一個:將多個小的 NALU(網絡抽象層單元)打包進一個 RTP 包中,以此來減少網絡開銷,提高傳輸效率。
簡單來說,STAP-A 就像一個大信封,可以把多封小信件(小的 NALU)裝在一起,然后只貼一張郵票(一個 RTP 頭部)寄出去。這比一封信貼一張郵票要劃算得多。
在 RTSP、RTSP over HTTP、SRT 等基于 RTP 的流媒體協議中,STAP-A 的作用至關重要,尤其是在傳輸分辨率高、需要頻繁發送參數集的視頻流時。
為什么需要 STAP-A?
在視頻編碼的世界里,除了包含實際圖像數據的視頻幀(如 IDR、I、P、B 幀),還有許多用于描述編碼參數的 NALU。這些參數通常非常小,比如:
-
VPS(Video Parameter Set):視頻參數集,描述多個序列的共享參數,特別是分層編碼結構。
-
SPS (Sequence Parameter Set):序列參數集,描述全局信息如分辨率、幀率、碼流級別。
-
PPS (Picture Parameter Set):圖像參數集,描述單幀或多幀的共享參數。
-
AUD (Access Unit Delimiter):訪問單元分隔符,標記一幀的開始。
-
SEI (Supplemental Enhancement Information):補充增強信息,包含時序等輔助數據。
這些 NALU 有時只有幾十字節。如果不使用 STAP-A,每個 NALU 都需要獨立封裝。一個 RTP 包至少有 12 字節的 RTP 頭部,加上 UDP/IP 頭部,總開銷通常超過 40 字節。對于一個 50 字節的 NALU 來說,協議開銷甚至比數據本身還要大。
STAP-A 的出現完美解決了這個問題。它將這些小 NALU 聚合起來,只需一個 RTP 頭部,就能傳輸多個 NALU,顯著降低了協議開銷(Protocol Overhead),從而節省了帶寬。
STAP-A 的封裝結構
一個使用 STAP-A 封裝的 RTP 包,其負載(Payload)結構如下:
- RTP 頭部:標準的 12 字節 RTP 頭部,包含序列號、時間戳等信息。
- STAP-A 指示器:一個 1 字節的特殊頭部,用于標識這是一個聚合包。
- 對于 H.264,其類型(Type)字段值為 24。
- 對于 H.265,其類型(Type)字段值為 48。
- 聚合 NALU 負載:由一個或多個 NALU 組成。特別的是,每個 NALU 在被放入負載之前,都會先加上一個 2 字節的大小字段。
下面是其結構示意圖:
+-----------------------------------+
| RTP Header |
+-----------------------------------+
| STAP-A Indicator | <- H.264: Type 24 / H.265: Type 48
+-------------------+---------------+
| NALU 1 Size | (2 bytes) |
+-------------------+---------------+
| NALU 1 Data |
+-------------------+---------------+
| NALU 2 Size | (2 bytes) |
+-------------------+---------------+
| NALU 2 Data |
+-------------------+---------------+
| ... and so on ... |
+-------------------+---------------+
STAP-A 的工作原理
當一個視頻流被編碼后,視頻發送端會有一個 RTP 封裝模塊。這個模塊會緩沖所有新生成的 NALU。它會根據 NALU 的大小和類型,決定如何封裝:
- 聚合判斷:如果隊列里有多個小的、非視頻幀 NALU(如 SPS、PPS),并且將它們打包后總大小不超過 MTU(通常為 1500 字節),那么模塊就會選擇 STAP-A 模式。
- 構建負載:
- 首先,創建一個新的 RTP 包,并寫入標準的 RTP 頭部。
- 其次,寫入 STAP-A 指示器(類型為 24 或 48)。
- 然后,對于每個要聚合的 NALU:
- 將該 NALU 的大小(2 字節,大端字節序)寫入負載。
- 將該 NALU 的完整數據寫入負載。
- 發送:當所有 NALU 都被封裝進一個 RTP 包后,該包通過網絡發送給接收端。
接收端的解析流程
接收端收到一個 RTP 包后,首先會解析其 RTP 頭部。然后,它會檢查負載的第一個字節來確定封裝類型:
- 識別類型:如果負載的第一個字節(去除
F
和NRI
位)是 24 或 48,接收端就知道這是一個 STAP-A 包。 - 逐個解包:
- 接收端進入一個循環,從負載的第二個字節開始。
- 讀取接下來的 2 個字節,得到第一個 NALU 的大小
L
。 - 讀取接下來的
L
個字節,得到第一個完整的 NALU 數據。 - 重復上述步驟,直到 RTP 包的負載數據被全部讀取完畢。
- 處理 NALU:接收端會將解包出的每個完整的 NALU 分發給相應的處理模塊,例如將 SPS 和 PPS 送給解碼器進行初始化。
STAP-A 的重要性與應用場景
STAP-A 是高效流媒體傳輸的關鍵,其應用場景主要有:
- 會話啟動:在建立 RTSP/RTMP/SRT 等會話時,服務器通常會用 STAP-A 將 SPS 和 PPS 打包發送給客戶端。這確保了客戶端可以在第一時間獲取所有必要的解碼參數,無需等待數據流中的關鍵幀。
- 參數更新:如果編碼參數在流媒體過程中發生變化(比如分辨率或幀率改變),新的 SPS/PPS 會被打包進 STAP-A 包中發送。
- 低開銷數據傳輸:任何小的、零散的 NALU(如 AUD、SEI)都可以通過 STAP-A 封裝,從而最大化網絡利用率。
STAP-A 與 FU-A 的關系
STAP-A 和 FU-A 是兩種互補而非競爭的機制。
- STAP-A 用于將多個小 NALU 聚合在一起,其目的是節省開銷。
- FU-A 用于將一個大 NALU 分片成小塊,其目的是適應 MTU 限制。
在實踐中,一個完整的 RTP 視頻流通常會同時使用這三種封裝模式:
- 單 NALU 模式:傳輸大多數普通的視頻幀(如 P/B 幀)。
- STAP-A 模式:傳輸 SPS/PPS 等參數集。
- FU-A 模式:傳輸大的關鍵幀(如 I/IDR 幀)。
STAP-A封包和解包示例
#include <iostream>
#include <vector>
#include <numeric>
#include <cstdint>
#include <stdexcept>// 模擬 RTP 數據包的有效載荷
// 在實際應用中,這部分數據將跟在 RTP 頭部之后
using RtpPayload = std::vector<uint8_t>;// 模擬 NALU 列表
using NalUnitList = std::vector<std::vector<uint8_t>>;// STAP-A H.264/H.265 類型值
#define H264_STAP_A_TYPE 24
#define H265_STAP_A_TYPE 48class StapAPacker {
public:// H.264 封裝: 將多個 H.264 NALU 聚合為一個 STAP-A 負載RtpPayload pack_h264_nalus(const NalUnitList& nalus) {if (nalus.empty()) {return RtpPayload();}RtpPayload payload;// 1. 寫入 STAP-A 指示器 (H.264: 類型 24)// 這里的 RefIdc 位可以根據第一個 NALU 的 RefIdc 來設置uint8_t stap_a_indicator = (nalus[0][0] & 0x60) | H264_STAP_A_TYPE;payload.push_back(stap_a_indicator);// 2. 寫入每個 NALU 的大小和數據for (const auto& nalu : nalus) {// NALU 大小 (2 字節, 大端字節序)uint16_t nalu_size = nalu.size();payload.push_back(static_cast<uint8_t>((nalu_size >> 8) & 0xFF));payload.push_back(static_cast<uint8_t>(nalu_size & 0xFF));// NALU 數據payload.insert(payload.end(), nalu.begin(), nalu.end());}return payload;}// H.265 封裝: 將多個 H.265 NALU 聚合為一個 STAP-A 負載RtpPayload pack_h265_nalus(const NalUnitList& nalus) {if (nalus.empty()) {return RtpPayload();}RtpPayload payload;// 1. 寫入 STAP-A 指示器 (H.265: 類型 48)// 使用第一個 NALU 的 2 字節頭部的 forbidden_zero_bit, layer_id, temporal_id 等uint8_t stap_a_indicator_byte0 = (nalus[0][0] & 0x81) | (H265_STAP_A_TYPE << 1);uint8_t stap_a_indicator_byte1 = nalus[0][1];payload.push_back(stap_a_indicator_byte0);payload.push_back(stap_a_indicator_byte1);// 2. 寫入每個 NALU 的大小和數據for (const auto& nalu : nalus) {// NALU 大小 (2 字節, 大端字節序)uint16_t nalu_size = nalu.size();payload.push_back(static_cast<uint8_t>((nalu_size >> 8) & 0xFF));payload.push_back(static_cast<uint8_t>(nalu_size & 0xFF));// NALU 數據payload.insert(payload.end(), nalu.begin(), nalu.end());}return payload;}// 解包: 從一個 STAP-A 負載中分離出所有 NALUNalUnitList unpack_stap_a(const RtpPayload& payload, bool is_h264) {NalUnitList nalus;size_t offset = 0;if (payload.empty()) {throw std::runtime_error("Payload is empty.");}// 1. 讀取并驗證 STAP-A 指示器if (is_h264) {uint8_t type = payload[0] & 0x1F;if (type != H264_STAP_A_TYPE) {throw std::runtime_error("Not a H.264 STAP-A packet.");}offset = 1;} else { // H.265uint8_t type = (payload[0] >> 1) & 0x3F;if (type != H265_STAP_A_TYPE) {throw std::runtime_error("Not a H.265 STAP-A packet.");}offset = 2; // H.265 STAP-A 頭部是 2 字節}// 2. 循環讀取每個 NALUwhile (offset < payload.size()) {// 檢查剩余數據是否足夠讀取 NALU 大小字段if (offset + 2 > payload.size()) {throw std::runtime_error("Truncated STAP-A packet: missing NALU size.");}// 讀取 NALU 大小uint16_t nalu_size = (payload[offset] << 8) | payload[offset + 1];offset += 2;// 檢查剩余數據是否足夠讀取整個 NALUif (offset + nalu_size > payload.size()) {throw std::runtime_error("Truncated STAP-A packet: NALU data incomplete.");}// 讀取 NALU 數據std::vector<uint8_t> nalu(payload.begin() + offset, payload.begin() + offset + nalu_size);nalus.push_back(nalu);offset += nalu_size;}return nalus;}
};void print_nalu_info(const std::vector<uint8_t>& nalu, bool is_h264) {if (nalu.empty()) return;uint8_t type = 0;if (is_h264) {type = nalu[0] & 0x1F;std::cout << " - H.264 NALU, Type: " << (int)type << ", Size: " << nalu.size() << " bytes." << std::endl;} else {type = (nalu[0] >> 1) & 0x3F;std::cout << " - H.265 NALU, Type: " << (int)type << ", Size: " << nalu.size() << " bytes." << std::endl;}
}int main() {StapAPacker packer;// --- 1. 模擬 H.264 NALU 聚合 ---std::cout << "--- H.264 STAP-A Aggregation Example ---" << std::endl;// 模擬 SPS (類型 7) 和 PPS (類型 8)NalUnitList h264_nalus_to_pack;std::vector<uint8_t> h264_sps = {0x67, 0x00, 0x40, 0x0a}; // 示例 SPSstd::vector<uint8_t> h264_pps = {0x68, 0xee, 0x01, 0x32}; // 示例 PPSh264_nalus_to_pack.push_back(h264_sps);h264_nalus_to_pack.push_back(h264_pps);RtpPayload h264_stap_a_payload = packer.pack_h264_nalus(h264_nalus_to_pack);std::cout << "H.264 Payload created, total size: " << h264_stap_a_payload.size() << " bytes." << std::endl;// --- 2. 模擬 H.264 解包 ---std::cout << "\n--- H.264 STAP-A Unpacking Example ---" << std::endl;try {NalUnitList unpacked_h264_nalus = packer.unpack_stap_a(h264_stap_a_payload, true);std::cout << "Successfully unpacked " << unpacked_h264_nalus.size() << " NALUs." << std::endl;for (const auto& nalu : unpacked_h264_nalus) {print_nalu_info(nalu, true);}} catch (const std::exception& e) {std::cerr << "Error unpacking H.264 payload: " << e.what() << std::endl;}// --- 3. 模擬 H.265 NALU 聚合 ---std::cout << "\n--- H.265 STAP-A Aggregation Example ---" << std::endl;// 模擬 VPS (類型 32), SPS (類型 33), PPS (類型 34)NalUnitList h265_nalus_to_pack;std::vector<uint8_t> h265_vps = {0x40, 0x01, 0x0a}; // 示例 VPSstd::vector<uint8_t> h265_sps = {0x42, 0x01, 0x01}; // 示例 SPSstd::vector<uint8_t> h265_pps = {0x44, 0x01, 0x01}; // 示例 PPSh265_nalus_to_pack.push_back(h265_vps);h265_nalus_to_pack.push_back(h265_sps);h265_nalus_to_pack.push_back(h265_pps);RtpPayload h265_stap_a_payload = packer.pack_h265_nalus(h265_nalus_to_pack);std::cout << "H.265 Payload created, total size: " << h265_stap_a_payload.size() << " bytes." << std::endl;// --- 4. 模擬 H.265 解包 ---std::cout << "\n--- H.265 STAP-A Unpacking Example ---" << std::endl;try {NalUnitList unpacked_h265_nalus = packer.unpack_stap_a(h265_stap_a_payload, false);std::cout << "Successfully unpacked " << unpacked_h265_nalus.size() << " NALUs." << std::endl;for (const auto& nalu : unpacked_h265_nalus) {print_nalu_info(nalu, false);}} catch (const std::exception& e) {std::cerr << "Error unpacking H.265 payload: " << e.what() << std::endl;}return 0;
}