音视频学习(五十八):STAP-A模式

什么是 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)结构如下:

  1. RTP 头部:标准的 12 字节 RTP 头部,包含序列号、时间戳等信息。
  2. STAP-A 指示器:一个 1 字节的特殊头部,用于标识这是一个聚合包。
    • 对于 H.264,其类型(Type)字段值为 24
    • 对于 H.265,其类型(Type)字段值为 48
  3. 聚合 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 的大小和类型,决定如何封装:

  1. 聚合判断:如果队列里有多个小的、非视频帧 NALU(如 SPS、PPS),并且将它们打包后总大小不超过 MTU(通常为 1500 字节),那么模块就会选择 STAP-A 模式。
  2. 构建负载
    • 首先,创建一个新的 RTP 包,并写入标准的 RTP 头部。
    • 其次,写入 STAP-A 指示器(类型为 24 或 48)。
    • 然后,对于每个要聚合的 NALU:
      • 将该 NALU 的大小(2 字节,大端字节序)写入负载。
      • 将该 NALU 的完整数据写入负载。
  3. 发送:当所有 NALU 都被封装进一个 RTP 包后,该包通过网络发送给接收端。

接收端的解析流程

接收端收到一个 RTP 包后,首先会解析其 RTP 头部。然后,它会检查负载的第一个字节来确定封装类型:

  1. 识别类型:如果负载的第一个字节(去除 FNRI 位)是 2448,接收端就知道这是一个 STAP-A 包。
  2. 逐个解包
    • 接收端进入一个循环,从负载的第二个字节开始。
    • 读取接下来的 2 个字节,得到第一个 NALU 的大小 L
    • 读取接下来的 L 个字节,得到第一个完整的 NALU 数据。
    • 重复上述步骤,直到 RTP 包的负载数据被全部读取完毕。
  3. 处理 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/920227.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/920227.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

管理型交换机通过VLAN划分实现不同IP跨网段通信配置方法

管理型交换机应用场景丰富&#xff0c;如果要实现不同IP跨网段通信(比如172.22.106.X和192.168.100.X实现通信)&#xff0c;通过VLAN划分是可以满足&#xff0c;下面分享基于弱三层交换机RTL9301方案核心模块SW-24G4F-301EM配置方法&#xff01; 1. 一般结合交换机的应用场景&a…

什么是高防服务器?如何进行防御?

高防服务器是指能为用户提供防御网络攻击&#xff0c;是主要针对DDOS等流量型攻击能力的服务器&#xff0c;通过部署专业的硬件设备与软件系统&#xff0c;具备高带宽、大流量清洗能力&#xff0c;能有效抵御各类恶意流量冲击&#xff0c;确保服务器稳定运行&#xff0c;保障网…

SW - 增加导出STL数据中的三角面数,增加别人逆向建模的难度

文章目录SW - 增加导出STL数据中的三角面数&#xff0c;增加别人逆向建模的难度概述笔记SW版本导出时&#xff0c;选择STL的导出选项默认导出(精细)导出粗糙自定义导出 - 将误差和角度改为最大自定义导出 - 将误差,角度,三角面数改为最大备注这几天的感想关于我不参考人家零件&…

四十一、【高级特性篇】API 文档驱动:OpenAPI/Swagger 一键导入测试用例

四十一、【高级特性篇】API 文档驱动:OpenAPI/Swagger 一键导入测试用例 前言 准备工作 第一部分:后端实现 - OpenAPI 解析与批量创建 API 1. 创建 OpenAPI 解析服务 2. 创建批量用例导入 API 3. 注册新 API 路由 第二部分:前端实现 - OpenAPI 导入界面 1. 更新 `api/testca…

K8S-Service资源对象

一、概述在kubernetes中&#xff0c;pod是应用程序的载体&#xff0c;我们可以通过pod的ip来访问应用程序&#xff0c;但是pod的ip地址不是固定的&#xff0c;这也就意味着不方便直接采用pod的ip对服务进行访问。为了解决这个问题&#xff0c;kubernetes提供了Service资源&…

【STM32】CubeMX(十三):RT-THREAD

本篇博客描述的是 RT-Thread STM32 CubeMX 的使用方法。本文也为大家提供了基于 STM32 使用 CubeMX 添加 RT-Thread 并创建闪烁 LED 任务 的操作流程。 便于您更好的理解。 一、RT-Thread 是什么&#xff1f; RT-Thread 是一个开源、轻量级的实时操作系统&#xff0c;适用于…

基于Ubuntu22.04系统PaddleX和PaddleClas训练推理MMAFEDB人脸表情识别数据集(详细教程)

目录 基于Ubuntu22.04系统PaddleX和PaddleClas训练推理MMAFEDB人脸表情识别数据集(详细教程) 超实用的Paddle图像分类训练推理教程&#xff0c;助力深度学习研究&#xff01; 1、环境准备(重要⭐⭐⭐) 构建虚拟环境 安装PaddlePaddle 安装PaddleX 安装PaddleClas插件 2…

Mistral AI音频大模型Voxtral解读

1. 引言 传统的语音处理系统(如OpenAI的Whisper)在ASR任务上取得了巨大成功,能将语音高精度地转换为文本。但这只是第一步。真正的“语音理解”意味着: 内容推理:不仅知道说了什么,还能理解话语背后的含义、情感和意图。 长篇摘要:能够听完一段长达数十分钟的播客或会议…

使用Docker+WordPress部署个人博客

一、通过docker compose 自动一键部署WordPress 1. 准备工作 安装 Docker 和 Docker Compose确保服务器有公网 IP&#xff08;如果需要外部访问&#xff09;域名&#xff08;可选&#xff0c;用于绑定网站&#xff09; 2. 创建 Docker Compose 配置文件 创建一个docker-compose…

http与https配置

Web 服务详解&#xff1a;HTTP 与 HTTPS 配置 一、HTTP 服务概述 HTTP&#xff08;Hypertext Transfer Protocol&#xff0c;超文本传输协议&#xff09;是用于在网络上传输网页数据的基础协议&#xff0c;默认使用80 端口&#xff0c;以明文形式传输数据。常见的 HTTP 服务软件…

Python爬虫实战:研究amazon-scrapy,构建亚马逊电商数据采集和分析系统

1 引言 1.1 研究背景 电子商务的高速发展使电商平台成为数据价值的核心载体。亚马逊作为全球领先的电商生态,截至 2024 年第二季度,其平台商品总量突破 1.5 亿,日均活跃用户超 3 亿,每日产生 PB 级的交易数据与用户行为记录。这些数据包含商品特征(价格、规格、品牌)、…

基于ERNIE 4.5的多智能体协作的自动化视频舆情分析报告生成器

多智能体协作的自动化视频舆情分析报告生成器 1. 项目的意义与价值 从“非结构化视频”中挖掘“结构化洞察”的通用挑战 在当今的数字生态中&#xff0c;视频已成为信息传播、知识分享和消费者意见表达的核心媒介。从企业内部的会议录屏、技术培训&#xff0c;到外部的市场宣传…

Java全栈开发面试实录:从基础到实战的深度解析

Java全栈开发面试实录&#xff1a;从基础到实战的深度解析 面试官与应聘者的对话记录 第一轮&#xff1a;基础问题与项目背景 面试官&#xff08;中年男性&#xff0c;穿着整洁&#xff09;&#xff1a; 你好&#xff0c;欢迎来到我们公司。我是今天的面试官&#xff0c;可以先…

如何清除webview138、139版本软键盘占用的区域

好的&#xff0c;这个问题非常具体且关键。在 Android System WebView 的 138 和 139 版本&#xff08;基于 Chromium 113&#xff09;的上下文中&#xff0c;“清除软键盘占用的区域”通常意味着&#xff1a;在软键盘收起后&#xff0c;WebView 的布局或视口没有正确恢复&…

深度学习:卷积神经网络(CNN)

文章目录一、CNN 基础认知1.1 图像在计算机中的存储形式1.2 图像识别的核心需求&#xff1a;画面不变性1.3 传统神经网络的局限二、CNN 核心原理&#xff1a;三大核心层与关键操作2.1 卷积层&#xff08;1&#xff09;什么是卷积操作&#xff1f;&#xff08;2&#xff09;卷积…

iOS 26 正式版即将发布,Flutter 完成全新 devicectl + lldb 的 Debug JIT 运行支持

在之前的 《Flutter 又双叒叕可以在 iOS 26 的真机上 hotload》 和 《Flutter 在 iOS 真机 Debug 运行出现 Timed out *** to update》 我们聊过&#xff0c;由于 iOS 26 开始&#xff0c;Apple 正式禁止了 Debug 时 mprotect 的 RX 权限&#xff0c;导致了 Flutter 在 Debug 运…

机器学习全流程拆解 _ 从数据到模型的科学之道

-—— 避开80%项目失败的隐形成本&#xff0c;掌握高效建模方法论*&#x1f4cc; 一、明确目标&#xff1a;成败的起点 1. 问题定位 分类任务&#xff1a;区分二分类/多分类/多标签分类预测任务&#xff1a;标量预测&#xff08;如房价&#xff09;vs 向量预测&#xff08;如股…

Android 广告轮播全实现:图片与视频混合展示的完整方案

广告轮播是移动应用中提升用户转化率的核心组件&#xff0c;尤其在电商、资讯类应用中应用广泛。传统轮播仅支持图片展示&#xff0c;而现代应用需要兼顾图片和视频内容以增强吸引力。本文将详细讲解如何实现一个支持图片与视频混合播放的高性能广告轮播&#xff0c;涵盖布局设…

AI大模型企业落地指南-笔记01

前言AI技术的发展趋势必然是越来越普及&#xff0c;越来越“技术平权”的。在未来10年内&#xff0c;AI将以各种方式“融入”人类世界&#xff0c;与人类乃至世界深度融合。一. 概念第1章 AI与大模型概述1.1 什么是AI人工智能&#xff08;全称Artificial Intelligence&#xff…

Linux-孤儿进程和僵死进程

文章目录孤儿进程概述僵死进程概述孤儿进程 概述 父进程运行结束&#xff0c;子进程还在运行&#xff0c;此时&#xff0c;子进程就成了孤儿进程&#xff08;Orphan Process&#xff09;每当出现一个孤儿进程的时候&#xff0c;内核就把孤儿进程的父进程设置为 init &#xf…