当前位置: 首页 > news >正文

H.264 编码原理与 RTP/RTSP 传输流程详解

H.264 编码原理与 RTP/RTSP 传输流程详解

  • 第一部分:H.264 编码结构
    • H.264 简介
    • 出现背景
    • 编码结构层次
    • 帧 (I、P、B 帧)
  • NALU (Network Abstraction Layer Unit)(重点)
    • VCL 层(视频编码层)
    • NAL 层(网络抽象层)
    • NALU 头部字段(F、NRI、Type)与起始码
    • 数据流封装
    • Annex-B 格式
    • AVCC 格式 (mp4/flv 内常见)
  • 第二部分:RTP 封包
    • RTP 协议简介
    • RTP header 基本字段(序列号、时间戳、payload type)
    • H.264 与 RTP 的结合
      • H.264 与 RTP 的封装过程:
      • H.264 RTP 流的典型结构:
    • FFMpeg编码NALU
    • 单 NALU 模式(一个 RTP 包放一个 NALU)
    • 分片模式 FU-A(大 NALU 被切片)
      • 去掉起始码
    • RTP 打包流程示例
      • 输入:H.264 NALU
      • 输出:RTP 包(头 + 负载)
      • wireshark 看rtp包 判断包类型
  • 第三部分:RTSP 发送
  • RTSP 协议与 H.264/RTP 实战速查
    • 1) RTSP 协议简介
    • 2) 常见命令(Methods)
    • 3) RTSP 会话建立流程(典型)
    • 4) 客户端请求 → 服务器响应(示例)
    • 5) SDP 关键字段(H.264 例)
    • 6) 数据发送过程(H.264 → RTP)
    • 7) RTP over TCP(interleaved)说明
    • 8) RTSP × H.264/RTP 示例(要点)
    • 9) 附:强制关键帧(FFmpeg/libx264 小贴士)
    • 10) 术语速记
      • 参考抓包操作

第一部分:H.264 编码结构

H.264 简介

H.264(也称为 AVC,Advanced Video Coding)是目前应用最广泛的视频压缩标准之一。无论是在线视频、监控摄像头,还是视频会议和直播,大多数场景都离不开 H.264。相比早期的 MPEG-2、MPEG-4 ASP 等标准,H.264 在保持视频质量的同时显著降低了码率,通常能在同等画质下节省 30%~50% 的带宽。这一特性使它成为流媒体传输和存储领域的事实标准。

出现背景

在互联网早期,视频的传输和存储成本非常高。MPEG-2 主要用于 DVD 和电视广播,码率通常在 2Mbps~8Mbps,对网络环境要求过高。而随着互联网视频和实时通信的兴起,人们迫切需要一种压缩效率更高、延迟更低的编码方式。H.264 正是在这一背景下由 ITU-T VCEG 和 ISO/IEC MPEG 联合制定,于 2003 年发布。
它的出现解决了两个关键问题:

  1. 带宽不足:在有限的网络带宽下依然能够传输清晰视频。

  2. 存储成本高:降低码率意味着同样大小的硬盘可以存更多视频内容。

因此,H.264 一经推出,就迅速成为视频应用的首选标准。

编码结构层次

H.264 的编码结构是分层次的,从宏观到微观,大体可以分为三个层面:

  1. 帧级(Frame):视频由一帧帧静态图像组成。
  2. 片级(Slice):每一帧可以被划分为若干个 slice,方便分割和传输。
  3. 宏块(Macroblock):在片内部,视频进一步划分为 16×16 的像素块,是编码的基本单位。
    在这里插入图片描述

这套层次化的设计,让 H.264 能够灵活地在不同应用场景下平衡效率与质量。

帧 (I、P、B 帧)

在 H.264 中,最重要的就是对不同帧类型的划分。不同类型的帧承担不同的编码和解码职责,通过它们的配合来降低冗余、提升压缩率。

I 帧(Intra-coded frame)
也叫关键帧,它是独立编码的帧,不依赖于其他帧。I 帧保存了完整的画面信息,相当于一张完整的图片。它的作用主要有两个:

提供解码起点,播放器只要拿到 I 帧就能正确显示画面;

在画质上起到“锚点”作用,保证视频解码的参考质量。
不过,I 帧的数据量最大,因为没有利用到时间上的冗余。

P 帧(Predicted frame)
依赖之前的 I 帧或 P 帧,通过运动预测和补偿来只保存变化部分。比如视频中一个人说话,背景几乎不变,那么 P 帧只需要记录嘴巴的变化信息即可。这大大减少了码率。
特点:压缩率较高,解码时必须依赖参考帧。

B 帧(Bi-predictive frame)
同时参考前后的 I/P 帧来进行双向预测。B 帧的压缩效率最高,但解码复杂度也最大,需要解码器缓存更多帧才能正确还原。
特点:能进一步降低码率,但会增加延迟,因此在实时性要求高的场景(如视频会议)通常较少使用

NALU (Network Abstraction Layer Unit)(重点)

无论是存储还是网络传输,H264 原始码流是由一个接一个 NALU(NAL Unit) 组成,它的功能分为两层,VCL(Video Coding Layer)视频编码层和 NAL(Network Abstraction Layer)网络提取层。

VCL 层(视频编码层)

VCL:包括核心压缩引擎和块、宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码;

NAL 层(网络抽象层)

NAL:负责将 VCL 产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别;

注意 VCL层不需要研究,我们网络传输需要重点看NAL。
NALU其实就是NAL Unit,也就是说nalu其实就是nal组成的,而h264码流就是这样一个一个nalu组成
在这里插入图片描述
sps和pps是码流信息描述,在整个文件中可以只有一组sps和pps

从上图我们可以知道,一张图片可以有多个NALU; 对解码器来说,需要先收到SPS和PPS进行初始化,否则解码器无法解出正常的帧数据。
发I帧之前,至少要发送一次SPS和PPS,因此如果在实际应用中遇到H264无法解码的时候,检查SPS和PPS是否有接收到并正常初始化。

NALU 头部字段(F、NRI、Type)与起始码

起始码作为nalu的起始在每个NALU前加上00 00 00 01 或00 00 01
在这里插入图片描述
orbidden_zero_bit (F,占1bit)
在 H.264 规范中规定了这⼀位必须为 0 。
nal_ref_idc (NRI,占2bit)
NAL重要性,值越大,越重要,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU,而不影响图像的回放 。 如果当前NALU是属于参考帧的片,或是序列参数集,或是图像参数集这些重要的单位时,本句法元素必需大于0。
nal_unit_type(Type,占5bit):
这个NALU单元的类型,1~12由H.264使用,24~31由H.264以外的应用使用。

NALU类型的详细解析表 :
NAL单元类型参考表

nal_unit_type描述
0未指定
1一个非IDR图像的编码条带(P帧/B帧)
2编码条带数据分割块A
3编码条带数据分割块B
4编码条带数据分割块C
5IDR图像的编码条带(IDR帧)
6辅助增强信息(SEI)
7序列参数集(SPS)
8图像参数集(PPS)
9访问单元分隔符
10序列结尾
11流结尾
12填充数据
13序列参数集扩展
14-18保留
19未分割的辅助编码图像的编码条带
20-23保留
24-31未指定

常见NALU类型对照表

十六进制二进制类型重要性类型值
0x670 11 00111SPS非常重要type = 7
0x680 11 01000PPS非常重要type = 8
0x650 11 00101IDR帧关键帧 非常重要type = 5
0x610 11 00001I帧非常重要type = 1
0x410 10 00001P帧重要type = 1
0x010 00 00001B帧不重要type = 1
0x060 00 00110SEI不重要type = 6

我们只需要记住类型为1 5678 即p i帧 和sei sps pps
在 十六进制中 ,开始符开始之后,第二个十六进制数就是nal类型
我们以一个h264文件看一下
在这里插入图片描述
在这里 起始码为00 00 01 47的二进制位 0 100 0111 这就是sps
在这里插入图片描述
在这里发现是65 就是关键帧 。学会这个很重要

数据流封装

在 H.264 编码过程中,视频数据从原始的 YUV 格式转换为压缩后的 ES(Elementary Stream) 格式。为了便于传输和存储,这些压缩后的数据需要进行封装。封装的主要任务是将编码后的数据组织成适合传输、存储的格式。在 H.264 中,常见的数据流封装格式包括 Annex-B 格式 和 AVCC 格式。这两种格式的选择主要取决于视频数据的应用场景。

Annex-B 格式

Annex-B 格式 是 H.264 编码流的一种原始封装格式,广泛用于实时视频流的传输(如 RTP、RTSP)。它通过 起始码(Start Code) 标记每个 NALU(Network Abstraction Layer Unit) 的边界,便于视频数据的同步和传输。
格式结构
起始码(Start Code):每个 NALU 前都有一个 起始码,通常是 0x000001 或 0x00000001,用于标识 NALU 的开始位置。
NALU 数据:紧随其后的部分即为 NALU 的内容,包含了视频数据和头信息(例如 SPS、PPS、slice 等)。
优点
同步性强:通过起始码标记 NALU 边界,解码器可以很容易地解析数据流。
简单易用:结构简单,容易理解和实现,适用于实时传输。
缺点
冗余数据:每个 NALU 前都需要添加起始码,增加了传输的开销。
不适合存储:由于每个 NALU 都有一个起始码,这使得其在存储时效率较低。
应用场景
实时流媒体传输,例如使用 RTP 或 RTSP 协议传输视频数据时,通常采用 Annex-B 格式。

AVCC 格式 (mp4/flv 内常见)

AVCC 格式 是另一种常见的封装格式,主要用于 文件存储,如 MP4、FLV 等容器格式。在 AVCC 格式 中,每个 NALU 前会有一个 4 字节的长度字段,用来表示该 NALU 的大小。这种格式适合存储大量视频数据,并且具有较低的传输开销。

第二部分:RTP 封包

RTP 协议简介

RTP(Real-time Transport Protocol) 是一种专门用于 实时数据传输 的协议,广泛应用于 语音、视频、音频 等流媒体的网络传输。RTP 是一个端到端协议,通常与 RTCP(Real-time Transport Control Protocol) 配合使用,后者负责反馈传输状态和控制信息。

RTP 主要目标是确保在 低延迟 和 实时性要求高的应用(如 视频会议、直播流、VoIP)中,数据可以顺利地传输,并尽可能减少丢包和时延。

RTP 的特点:

  • 端到端传输:直接在源和接收端之间传输数据。

  • 不保证可靠性:RTP 不会对丢失的数据进行重传,它通常用于 实时传输,丢包是可以容忍的。

  • 带有时间戳:为每个数据包添加时间戳,确保数据按时间顺序播放。

  • 支持多媒体数据:能够传输音频、视频以及其他数据流。

RTP 协议通过将媒体数据分成小的数据包(packet)进行传输,每个数据包都有特定的头部和载荷部分,用于标识包的类型和传输信息

RTP header 基本字段(序列号、时间戳、payload type)

字段大小 (bit)说明
版本号 (Version)2表示 RTP 的版本号,当前为 2
填充标志 (Padding)1如果设置为 1,则表示 RTP 包包含填充字节
扩展标志 (Extension)1如果设置为 1,则表示有扩展头部
CSRC 数量 (CC)4指示 CSRC(贡献源)的数量
标志位 (Marker)1标记位,通常用于标记帧的开始(如 I 帧)
负载类型 (Payload Type)7指示 RTP 包的负载类型,通常用于指示编码格式(如 H.264、G.711)
序列号 (Sequence Number)16序列号,标识 RTP 包的顺序,接收端可以根据序列号检测丢包
时间戳 (Timestamp)32标识数据包的采样时间,帮助接收端同步数据流
同步源标识符 (SSRC)32唯一标识 RTP 会话中的同步源
贡献源标识符 (CSRC)0 或 32 × N如果使用了 CC,此字段用于标识数据源

主要字段解析:
版本号 (Version):指示 RTP 协议的版本,当前为 2。
负载类型 (Payload Type):指示 RTP 数据包中包含的数据类型。对于 H.264 视频流,Payload Type 通常会设置为某个特定的值,例如 96(根据 IETF RFC 3551 的建议)。接收端可以根据这个字段识别数据流的编码格式。
序列号 (Sequence Number):RTP 数据包的序列号是一个 递增的数字,帮助接收端检测丢包和恢复数据流顺序。如果某个包丢失,接收端会通过序列号检查缺失包的位置。
时间戳 (Timestamp):表示 RTP 包的采样时间点,通常对应于视频帧的显示时间。时间戳对于视频流的同步至关重要,确保帧按正确的顺序和时间间隔播放。
标志位 (Marker):标记位常用于指示特殊的时间点或数据包。对于视频流,标志位常常用于标识 I 帧(关键帧),让接收端知道何时需要进行新的解码序列。

H.264 与 RTP 的结合

H.264 视频流与 RTP 的结合,通常用于实时视频流的传输(如视频会议、网络直播等),它需要将 H.264 编码数据 按照 RTP 协议 打包,以便网络传输。H.264 编码视频数据通常以 NALU(Network Abstraction Layer Unit) 的形式存在,而 RTP 负责将这些 NALU 封装并在网络中传输。

H.264 与 RTP 的封装过程:

  1. NALU 生成

    • H.264 编码器将原始视频帧(YUV 格式)压缩为 H.264 视频流。这个流由多个 NALU 组成,其中每个 NALU 代表视频编码过程中的一个片段(如一个 slice)。
    • 每个 NALU 内部包含了压缩后的图像数据或编码参数(如 SPSPPS 等)。
  2. RTP 封装

    • RTP 协议负责将这些 NALU 数据分割成多个数据包,确保每个数据包带有相应的 序列号时间戳,以便接收端能够按顺序重组视频帧。
    • 为了适应 RTP 协议的传输要求,H.264 数据流可能会进行 分片(特别是当 NALU 较大时)。这种分片过程可以使用 FU-A(Fragmentation Unit A)等方法进行处理。
  3. RTP 与 H.264 的兼容性

    • RTP Payload Type:RTP 中的 Payload Type 字段用来标识 H.264 编码数据流。通常,Payload Type 为 96(根据 IETF RFC 3551),表示负载类型为 H.264 视频流。
    • NALU 封装:RTP 封装的 H.264 视频流 会根据 Annex-B 格式AVCC 格式 来处理 NALU,这取决于传输协议。一般来说,Annex-B 格式 更常见于流媒体传输。
  4. RTP 与 H.264 数据流同步

    • 时间戳:RTP 包中的 时间戳 用于指示该包的播放时间,这样接收端可以按照正确的时间顺序播放视频。
    • 序列号:RTP 包中的 序列号 用于确保数据包按顺序进行解码和播放,防止数据包丢失或乱序。

H.264 RTP 流的典型结构:

  • RTP 包 = RTP header + NALU Payload
    • RTP Header:包含 序列号时间戳Payload Type 等信息。
    • NALU Payload:即 H.264 编码数据,比如 sliceSPS/PPS 等。

FFMpeg编码NALU

我们在对NALU组包时 也需要知道,怎么把YUV变成NALU
[ 输入 NV12 YUV 数据 ]


sws_scale 转换
(NV12 → YUV420P)


avcodec_send_frame()
(送入编码器)


┌───────────────────────┐
│ x264 编码器逻辑 │
│ - GOP/参考帧 │
│ - I/P/B 帧决策 │
│ - 生成 NALU (VCL) │
│ - 插入 SPS/PPS/SEI │
└───────────────────────┘


avcodec_receive_packet()
(得到 AVPacket)

  • 解析 NALU 起始码
  • 保存 SPS/PPS


    [ 输出 H.264 Annex-B 码流 ]
    这里的转换是很重要的 ,我们在FFMpeg进行编码的时候需要把YUV格式 转换为YUV420进行从AVFRame----->AVPacket,当我们送入YUV 编码器会把YUV编码为IBP帧,并且组成nalu单元,输出ES码流存入AVPAcket,那么一个AVPacket存多少nalu呢 提供测试demo
// C++11 仅用:-std=c++11
// 链接:avcodec avutil (若不做像素转换,无需 swscale)
// Windows: 链接 avcodec.lib avutil.lib,并把对应 DLL 放到 exe 目录。
// Linux/macOS: g++ test.cpp -std=c++11 -lavcodec -lavutil -o test
#define  _CRT_SECURE_NO_WARNINGS#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <string>
#include <iostream>
#include <vector>extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>   // av_opt_set 在这里
}// ---------- NAL 类型到文本 ----------
static const char* h264_nal_type(int t) {switch (t) {case 1: return "Non-IDR slice (P/B)";case 5: return "IDR slice (I)";case 6: return "SEI";case 7: return "SPS";case 8: return "PPS";case 9: return "AUD";default: return "Other";}
}// ---------- Annex-B 起始码检测 ----------
static inline int startcode_len(const uint8_t* p, const uint8_t* end) {if (p + 3 <= end && p[0] == 0 && p[1] == 0 && p[2] == 1) return 3;if (p + 4 <= end && p[0] == 0 && p[1] == 0 && p[2] == 0 && p[3] == 1) return 4;return 0;
}// 解析并打印 NAL(优先按 Annex-B;不匹配则尝试 AVCC 4-byte length)
static void print_nalus_from_packet(const AVPacket* pkt) {const uint8_t* p = pkt->data;const uint8_t* end = pkt->data + pkt->size;// 尝试 Annex-Bconst uint8_t* s = p;int sc = 0;while (s < end && (sc = startcode_len(s, end)) == 0) ++s;if (sc) {const uint8_t* cur = s + sc;while (cur < end) {const uint8_t* q = cur;int sc2 = 0;while (q < end && (sc2 = startcode_len(q, end)) == 0) ++q;const uint8_t* nal = cur;const uint8_t* nal_end = (sc2 ? q : end);if (nal < nal_end) {uint8_t nal_type = nal[0] & 0x1F;std::cout << "  NAL type=" << (int)nal_type<< " (" << h264_nal_type((int)nal_type) << ")\n";}if (!sc2) break;cur = q + sc2;}return;}// 尝试 AVCC(4 字节长度前缀,大端)const uint8_t* cur = p;while (cur + 4 <= end) {uint32_t len = ((uint32_t)cur[0] << 24) | ((uint32_t)cur[1] << 16) |((uint32_t)cur[2] << 8) | (uint32_t)cur[3];cur += 4;if (len == 0 || cur + len > end) break;uint8_t nal_type = cur[0] & 0x1F;std::cout << "  NAL type=" << (int)nal_type<< " (" << h264_nal_type((int)nal_type) << ")\n";cur += len;}
}// 把连续平面 YUV420P(紧密存储)拷到 AVFrame(考虑 linesize)
static bool copy_frame_yuv420p(FILE* f, AVFrame* frame, int w, int h) {const int y_size = w * h;const int uv_w = w / 2;const int uv_h = h / 2;const int uv_size = uv_w * uv_h;std::vector<uint8_t> y(y_size), u(uv_size), v(uv_size);if (fread(&y[0], 1, y_size, f) != (size_t)y_size)  return false;if (fread(&u[0], 1, uv_size, f) != (size_t)uv_size) return false;if (fread(&v[0], 1, uv_size, f) != (size_t)uv_size) return false;if (av_frame_make_writable(frame) < 0) return false;// Yfor (int i = 0; i < h; ++i) {memcpy(frame->data[0] + i * frame->linesize[0],&y[0] + i * w, w);}// Ufor (int i = 0; i < uv_h; ++i) {memcpy(frame->data[1] + i * frame->linesize[1],&u[0] + i * uv_w, uv_w);}// Vfor (int i = 0; i < uv_h; ++i) {memcpy(frame->data[2] + i * frame->linesize[2],&v[0] + i * uv_w, uv_w);}return true;
}int main(int argc, char** argv) {// -------- 参数 --------std::string in_path = "output.yuv"; // 输入 YUV420P 文件int W = 320;int H = 240;int FPS = 25;int max_frames = 100;             // 读取的最多帧数(可按需改)if (argc >= 6) {in_path = argv[1];W = std::atoi(argv[2]);H = std::atoi(argv[3]);FPS = std::atoi(argv[4]);max_frames = std::atoi(argv[5]);}else {std::cout << "Usage: " << (argc > 0 ? argv[0] : "app")<< " <in.yuv> <W> <H> <FPS> <max_frames>\n"<< "Default: out.yuv 320 240 25 100\n";}FILE* fin = std::fopen(in_path.c_str(), "rb");if (!fin) {std::cerr << "open " << in_path << " failed\n";return 1;}// -------- 找编码器 --------const AVCodec* codec = avcodec_find_encoder_by_name("libx264");if (!codec) codec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!codec) {std::cerr << "no h264 encoder available\n";std::fclose(fin);return 1;}// -------- 创建编码器上下文 --------AVCodecContext* ctx = avcodec_alloc_context3(codec);if (!ctx) {std::cerr << "avcodec_alloc_context3 failed\n";std::fclose(fin);return 1;}ctx->width = W;ctx->height = H;ctx->pix_fmt = AV_PIX_FMT_YUV420P;ctx->gop_size = FPS;ctx->max_b_frames = 0;// 用 C++11 结构体赋值ctx->time_base = { 1, FPS };ctx->framerate = { FPS, 1 };// 低延迟/可观测配置(不同编码器可能忽略)av_opt_set(ctx->priv_data, "preset", "veryfast", 0);av_opt_set(ctx->priv_data, "tune", "zerolatency", 0);av_opt_set(ctx->priv_data, "repeat_headers", "1", 0); // 每个 IDR 前 SPS/PPSif (avcodec_open2(ctx, codec, NULL) < 0) {std::cerr << "avcodec_open2 failed\n";std::fclose(fin);avcodec_free_context(&ctx);return 1;}// -------- 准备帧 --------AVFrame* frame = av_frame_alloc();if (!frame) {std::cerr << "av_frame_alloc failed\n";std::fclose(fin);avcodec_free_context(&ctx);return 1;}frame->format = ctx->pix_fmt;frame->width = ctx->width;frame->height = ctx->height;if (av_frame_get_buffer(frame, 32) < 0) {std::cerr << "av_frame_get_buffer failed\n";std::fclose(fin);av_frame_free(&frame);avcodec_free_context(&ctx);return 1;}// -------- 读帧并编码 --------int64_t pts = 0;int frame_cnt = 0;while (frame_cnt < max_frames) {if (!copy_frame_yuv420p(fin, frame, W, H)) break;frame->pts = pts++;++frame_cnt;if (avcodec_send_frame(ctx, frame) < 0) {std::cerr << "avcodec_send_frame failed\n";break;}AVPacket* pkt = av_packet_alloc();if (!pkt) { std::cerr << "av_packet_alloc failed\n"; break; }while (1) {int r = avcodec_receive_packet(ctx, pkt);if (r == AVERROR(EAGAIN) || r == AVERROR_EOF) break;if (r < 0) {std::cerr << "avcodec_receive_packet error\n";break;}std::cout << "AVPacket pts=" << pkt->pts<< " size=" << pkt->size << "\n";print_nalus_from_packet(pkt);av_packet_unref(pkt);}av_packet_free(&pkt);}// -------- flush --------avcodec_send_frame(ctx, NULL);{AVPacket* pkt = av_packet_alloc();if (pkt) {while (1) {int r = avcodec_receive_packet(ctx, pkt);if (r == AVERROR(EAGAIN) || r == AVERROR_EOF) break;if (r < 0) break;std::cout << "FLUSH AVPacket pts=" << pkt->pts<< " size=" << pkt->size << "\n";print_nalus_from_packet(pkt);av_packet_unref(pkt);}av_packet_free(&pkt);}}// -------- 清理 --------std::fclose(fin);av_frame_free(&frame);avcodec_free_context(&ctx);return 0;
}

在这里插入图片描述
在这里插入图片描述
片的介绍如上图。

通过上面的介绍,YUV在通过编码器之后拿到了NALU ,NALU被封装在AVPacket里面。
在这里插入图片描述
由于FFMpeg已经帮我们进行了i帧分片,但是我们mtu最大是1500 ,我们可以设置不分片 如下图
在这里插入图片描述
不分片一个i帧很大,分3片也超过1500(MTU),我们可以设置

av_opt_set(ctx->priv_data, "x264-params", "slice-max-size=1500", 0);

1500还是偏大的,加上rtp头还是不合适的
在这里插入图片描述

单 NALU 模式(一个 RTP 包放一个 NALU)

当我们编码时,如果做了nalu分片,那么可以直接打包发送,但是大部分情况我们都没有分包那么精准,所以我们都是采用分片模式组包

分片模式 FU-A(大 NALU 被切片)

分片流程

去掉起始码

接口流程 输入AVPacket,对AVPacket中的NALU 去头 并存入pair<const uint8_t*, size_t> > 也可以使用结构体管理。
分离思路:
[00 00 00 01] 67 xx xx xx
[00 00 00 01] 68 yy yy
[00 00 01] 65 zz zz zz zz
找到开始码的起始位和结束位就可以进行分离

// ---------- 只分离:从 Annex-B 里切出裸 NALU(不含起始码) ----------
static void splitAnnexB(const uint8_t* data, size_t len,std::vector<std::pair<const uint8_t*, size_t> >& out)
{out.clear();if (!data || len < 4) return;size_t i = 0;size_t nal_start = (size_t)-1;// 找到第一个起始码,记录“起始码之后”作为 NAL 开头while (i + 3 < len) {if (i + 4 <= len && isStartCode4(data + i)) { i += 4; nal_start = i; break; }if (isStartCode3(data + i)) { i += 3; nal_start = i; break; }++i;}if (nal_start == (size_t)-1) { // 兜底:未找到起始码,整体作为一个片段out.push_back(std::make_pair(data, len));return;}// 继续扫描,遇到下一个起始码就切出前一个 NALUwhile (i + 3 < len) {bool sc4 = (i + 4 <= len) && isStartCode4(data + i);bool sc3 = isStartCode3(data + i);if (sc4 || sc3) {size_t nal_end = i;if (nal_end > nal_start) {out.push_back(std::make_pair(data + nal_start, nal_end - nal_start));}i += sc4 ? 4 : 3;     // 跳过起始码nal_start = i;        // 新 NAL 开始}else {++i;}}// 收尾:文件末尾/包末尾if (nal_start < len) {out.push_back(std::make_pair(data + nal_start, len - nal_start));}// 清理零长度(保险)std::vector<std::pair<const uint8_t*, size_t> >::iterator it = out.begin();while (it != out.end()) {if (it->second == 0) it = out.erase(it);else ++it;}
}

RTP 打包流程示例

输入:H.264 NALU

在这里插入图片描述
在这里插入图片描述
截取了两包AVFrame ,第一包AVframe,封装了 6个nalu 其中有SPS,PPS ,SEI以及和一个关键帧。
第二包AVFrame,封装了3nalu都是p帧。
这就是这两个AVFrame 里面NALU的情况。

输出:RTP 包(头 + 负载)

输出RTP包时,需要对NALU分片,以及填充rtp头 ,fu-a头。
±-----------±-----------±-----------±-----------+
| RTP Header (12 Bytes) | FU Indicator | FU Header |
±-----------±-----------±-----------±-----------+
| Fragmented NALU data … |
±-------------------------------------------------+
FU-a头有两个头FU Indicator 和FU Header。

RTP Header 只知道“我在传输第 N 个包”

FU Indicator 告诉你“这个包是FU-A”

FU Header 告诉你“这个分片是第一个/中间/最后一个”

一般FU-A的Indicator都为7C

分片FU Header二进制含义
第1包0x851000 0101S=1,E=0,表示“首片”,Type=5
第2包0x050000 0101S=0,E=0,表示“中片”,Type=5
第3包0x450100 0101S=0,E=1,表示“尾片”,Type=5

接下来进行组包rtp:

  1. 读取NALU头
位域bit7bit6-bit5bit4-bit3-bit2-bit1-bit0
名称FNRINUT
大小1 bit2 bit5 bit

nalh = nal[0] 含义:原始 NALU 的 1 字节头,里面同时包含了 F / NRI / NUT 三个字段的所有位。
作用:作为源字节,用来提取下面三个字段。

NUT = (nalh & 0x1F) 含义:NAL Unit Type(低 5 位),范围 0–31。 作用:指明 NALU
的“内容/用途”,解码与打包策略主要看它。常见值:

1:非 IDR 片(P/B slice)

5:IDR 片(关键帧 slice)

6:SEI

7:SPS

8:PPS

9:AUD

24:STAP-A(聚合包)

28:FU-A(分片单元)

NRI = (nalh & 0x60) 含义:nal_ref_idc(位 5–6,共 2 位),代表参考重要性,取值 0–3(在字节里就是
0、0x20、0x40、0x60)。 作用:指示这个 NALU 是否会被参考/重要程度,通常:

0:非参考(如 SEI/AUD 多为 0)

1–3:有参考价值,数值越大“越重要”(如 SPS/PPS 常见为 3) 在你的代码里保留 未右移 的形式(仍占据
bit5–6),便于直接和别的“低 5 位类型值”按位或,组成新的字节(见下文 FU-A)。

Fbit = (nalh & 0x80) 含义:forbidden_zero_bit(位 7,最高位)。标准要求必须为 0。 作用:一旦为
1,表示该 NALU/比特流出现错误或被标记为非法;接收端可以据此丢弃该 NALU。实际流里几乎总是 0。

 if (!nal || nal_len == 0 || max_payload == 0) return;   // 基本健壮性校验const uint8_t nalh = nal[0];            // 原始 1B NAL 头(F|NRI|Type)const uint8_t NUT = (nalh & 0x1F);      // 原始 Type(低5位)const uint8_t NRI = (nalh & 0x60);      // 原始 NRI(重要性)const uint8_t Fbit = (nalh & 0x80);     // 原始 F 位(违规标记,通常为 0)
  1. 如果需要分包填写FU-A
    图 4 表示FU-A的RTP荷载格式。FU-A由1字节的分片单元指示(如图5),1字节的分片单元头(如图6),和分片单元荷载组成。
    在这里插入图片描述

图中左边(图5)—— FU Indicator(分片单元指示符) 这一字节在每个 FU-A 包开头都会出现。 位段 名称 说明 F (bit 0) forbidden_zero_bit 固定为 0(若为 1 表示该NALU有错误) NRI (bit 1–2) nal_ref_idc 表示 NALU 的重要程度(0~3,数值越大越重要) Type (bit 3–7) nal_unit_type 在 FU-A 中固定为 28(即 11100₂)表示这是“分片单元” 这个字节 =原始NALU头的前3位(F,NRI) + 新Type(28)。 它告诉接收端:这不是完整NALU,而是一个FU-A分片。
图中右边(图6)—— FU Header(分片单元头) 这是 FU-A结构中的第二个字节,用来说明**当前分片的位置(首片/中间/尾片)**以及原始NALU的类型。 位段 名称 说明 S (bit 0) Start =1 表示本片是 NALU 的第一个分片 E (bit 1) End =1 表示本片是NALU 的最后一个分片 R (bit 2) Reserved 保留位,固定为 0 Type (bit3–7) nal_unit_type 原始 NALU 的类型(例如 1=非IDR,5=IDR,7=SPS,8=PPS 等)

const uint8_t FU_IND = (uint8_t)(Fbit | NRI | 28);      // FU-Indicator: F|NRI|Type=28

对于有效NALU的 FU_IND为 124(7C)。
在这里插入图片描述
对于FU_ Header

分片FU Header二进制含义
第1包0x851000 0101S=1,E=0,表示“首片”,Type=5
第2包0x050000 0101S=0,E=0,表示“中片”,Type=5
第3包0x450100 0101S=0,E=1,表示“尾片”,Type=5
分片FU Header二进制含义
第1包0x811000 0001S=1, E=0,首片,Type=1(非IDR:P/B)
第2包0x010000 0001S=0, E=0,中片,Type=1
第3包0x410100 0001S=0, E=1,尾片,Type=1

FU-Header字段的Type就是NALU的NUT
FU-Header=SER|NUT(NALU)
其中如果是最后一片RTP的头 M=1。

总结 这里对写入 RTP包的首数据常见做了总结

十六进制二进制(8位)低5位(Type,二进制)类型值(十进制)
7C0111 110011100分片 FU-A28
670110 011100111不分片 SPS7
680110 100001000不分片 PPS8

wireshark 看rtp包 判断包类型

在这里插入图片描述
首包 41 ,则是nalu整个 并且为bp帧

void H264RtpServer::sendNal_(const uint8_t* nalu, size_t nalu_len, uint32_t ts90k, bool marker) {if (!nalu || nalu_len == 0) return;                            // 校验int fd = cfd_;                                                 // 读取当前客户端 fdif (fd < 0) return;                                            // 无连接则返回const size_t MAX_PAYLOAD = 1400;                               // 单个 RTP 载荷上限(不含 12B 头)std::lock_guard<std::mutex> lk(send_mtx_);                     // 发送保护(互斥)if (nalu_len <= MAX_PAYLOAD) {                                 // ---- Single NAL ----auto rtp = make_rtp_header(marker, seq_++, ts90k, ssrc_);  // 12B RTP 固定头(M=marker)uint16_t rtp_len = uint16_t(12 + nalu_len);                // RTP 总长度std::vector<uint8_t> pkt;                                  // interleaved 块缓冲pkt.reserve(4 + rtp_len);                                  // 预留空间:4B + RTPpkt.push_back('$'); pkt.push_back(0);                      // '$' + channel=0(RTP)pkt.push_back(uint8_t(rtp_len >> 8));                      // 长度高位(大端)pkt.push_back(uint8_t(rtp_len & 0xFF));                    // 长度低位pkt.insert(pkt.end(), rtp.begin(), rtp.end());             // RTP 头pkt.insert(pkt.end(), nalu, nalu + nalu_len);              // 载荷=完整 NALU(void)send_all(fd, pkt.data(), pkt.size());                // 发送整个 interleaved 块} else {                                                       // ---- FU-A 分片 ----const uint8_t h   = nalu[0];                               // 原始 NAL 头const uint8_t F   = (h & 0x80);                            // F 位const uint8_t NRI = (h & 0x60);                            // NRI(重要性)const uint8_t NUT = (h & 0x1F);                            // 原始类型(低 5 位)const uint8_t FU_INDICATOR = F | NRI | 28;                 // FU-Indicator: F|NRI|Type=28size_t remain = nalu_len - 1;                              // 剩余 RBSP 字节数(去掉 1B 头)const uint8_t* p = nalu + 1;                               // 分片起点bool start = true;                                         // 首片标记while (remain > 0) {                                       // 循环切片size_t chunk = std::min(remain, MAX_PAYLOAD - 2);      // 本片大小(扣除 2B FU 头)bool end = (chunk == remain);                          // 是否最后一片(决定 E 位/M 位)uint8_t fu_header = (start ? 0x80 : 0x00)              // FU-Header.S(bit7)| (end ? 0x40 : 0x00)                // FU-Header.E(bit6)| (NUT & 0x1F);                      // FU-Header.NUT(低5位)auto rtp = make_rtp_header(marker && end, seq_++, ts90k, ssrc_); // 仅尾片且帧末置 Muint16_t rtp_len = uint16_t(12 + 2 + chunk);           // RTP 总长度(12+FU头2+数据)std::vector<uint8_t> pkt;                              // interleaved 块pkt.reserve(4 + rtp_len);                              // 预留pkt.push_back('$'); pkt.push_back(0);                  // '$'+ch=0pkt.push_back(uint8_t(rtp_len >> 8));                  // 长度高位pkt.push_back(uint8_t(rtp_len & 0xFF));                // 长度低位pkt.insert(pkt.end(), rtp.begin(), rtp.end());         // RTP 头pkt.push_back(FU_INDICATOR);                           // 1B FU-Indicatorpkt.push_back(fu_header);                              // 1B FU-Headerpkt.insert(pkt.end(), p, p + chunk);                   // 分片载荷if (!send_all(fd, pkt.data(), pkt.size())) break;      // 发送失败则中断p += chunk; remain -= chunk; start = false;            // 前进到下一片}}
}

第三部分:RTSP 发送

RTSP 协议与 H.264/RTP 实战速查

面向工程实现与抓包调试的简明笔记。涵盖常见 RTSP 命令、会话时序、SDP 字段、RTP over TCP(interleaved)封装,以及 H.264/RTP 负载化要点。


1) RTSP 协议简介

  • 用途:控制媒体会话(播放/暂停/位置跳转等),本身不承载媒体数据。
  • 传输层:通常走 TCP 554(也可其他端口)。媒体数据常用 RTP/UDP;也可 RTP over TCP(interleaved)
  • 与 HTTP 的区别:都是基于文本的请求/响应协议,但 RTSP 是面向会话媒体控制

2) 常见命令(Methods)

方法作用简述常见关键头
OPTIONS询问服务器支持的 RTSP 方法CSeq
DESCRIBE请求 SDP 描述,了解媒体编码/时钟等参数CSeqAccept: application/sdp
SETUP建立传输通道(UDP 或 RTP/AVP/TCP interleaved)CSeqTransport、(成功后响应里会返回 Session
PLAY开始发送媒体流CSeqSession、(响应可含 RTP-Info 初始 seq/rtptime
PAUSE暂停播放CSeqSession
TEARDOWN结束会话CSeqSession
GET_PARAMETER保活/心跳(可选)CSeqSession

最小必需头CSeq(序号)、Session(从 SETUP 响应获取,后续请求需带)。


3) RTSP 会话建立流程(典型)

sequenceDiagramautonumberparticipant C as Clientparticipant S as ServerC->>S: OPTIONS rtsp://host/pathS-->>C: 200 OK (Public: ...)C->>S: DESCRIBE rtsp://host/path (Accept: application/sdp)S-->>C: 200 OK + SDPC->>S: SETUP rtsp://host/path/track1 (Transport: RTP/AVP/TCP;interleaved=0-1)S-->>C: 200 OK (Transport: ...;ssrc=..., Session: ...)C->>S: PLAY rtsp://host/path (Session: ...)S-->>C: 200 OK (RTP-Info: seq=..., rtptime=...)Note over C,S: S 开始以 interleaved 模式在 TCP 流中发送:<br/>"$" + ch + len + RTP

若是 RTP/UDP,SETUP 阶段会协商客户端/服务器的 UDP 端口;PLAY 后媒体数据走 UDP。


4) 客户端请求 → 服务器响应(示例)

OPTIONS

OPTIONS rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 1--
RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE

DESCRIBE

DESCRIBE rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 2
Accept: application/sdp--
RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: <len>v=0
o=- 0 0 IN IP4 10.0.0.1
s=H264
t=0 0
a=control:*
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;sprop-parameter-sets=<b64-sps>,<b64-pps>
a=control:track1

SETUP(RTP over TCP)

SETUP rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 3
Transport: RTP/AVP/TCP;unicast;interleaved=0-1--
RTSP/1.0 200 OK
CSeq: 3
Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=12345678
Session: 12345678

PLAY

PLAY rtsp://10.0.0.1/track1 RTSP/1.0
CSeq: 4
Session: 12345678--
RTSP/1.0 200 OK
CSeq: 4
RTP-Info: url=rtsp://10.0.0.1/track1;seq=1000;rtptime=0
Session: 12345678

5) SDP 关键字段(H.264 例)

  • m=video 0 RTP/AVP 96:媒体类型=video,控制端口占位 0(TCP 模式不在 SDP 暴露端口),PT=96(动态负载)。
  • a=rtpmap:96 H264/90000:将 PT=96 映射为 H.264,时钟 90 kHz(RTP 时间戳单位)。
  • a=fmtp:96 packetization-mode=1; sprop-parameter-sets=...
    • packetization-mode0=单 NAL;1=Single NAL/STAP-A/FU-A;2=非常用(STAP-B/MTAP)。
    • sprop-parameter-sets:SPS/PPS 的 Base64,用于解码器初始化。
  • a=control:track1:该媒体的 RTSP 控制 URL 后缀。

SPS/PPS Base64:直接对 Annex‑B 中去掉起始码的 SPS/PPS 字节做 Base64。


6) 数据发送过程(H.264 → RTP)

  1. 编码器输出 Annex‑B 字节流:00 00 00 01 + NALU(SPS/PPS/SEI/IDR/P 等)。
  2. 将起始码剥离,逐个 NALU 做 RTP 负载化:
    • Single NAL Unit:小 NALU(如 SPS/PPS/小 P 帧片段)直接一个 RTP 包携带。
    • STAP‑A (type=24):把多个小 NALU 合并到一个 RTP 包(很少用)。
    • FU‑A (type=28):大 NALU 分片:载荷 = FU-Indicator(1B) + FU-Header(1B) + 片数据
      • FU-HeaderS=首片(bit7), E=尾片(bit6), Type=原始 NAL 类型(低5位)。
  3. RTP 头(12 B):V=2PT=96(由 SDP 指定),seq 递增,ts(90 kHz 时钟,通常按帧步进),ssrc 唯一。
  4. Marker(M) 位:通常置于“一帧的最后一个 RTP 包”。
  5. RTP over TCP(interleaved)'$'(0x24) + channel(1B) + length(2B-BE) + RTP(12B+payload)

7) RTP over TCP(interleaved)说明

  • 动机:穿越 NAT/防火墙,复用 RTSP 的 TCP 连接承载 RTP/RTCP。
  • 帧格式
'$' 1B | Channel 1B | Length 2B (大端,=后续RTP字节数) | RTP(12B+payload)
  • 通道:常见 0=RTP1=RTCP(也可协商其他)。
  • 示例(十六进制)24 00 04 B0 ...
    • 0x24=‘$’,0x00=RTP 通道,0x04B0=1200(接下来 1200 字节是 RTP)。

8) RTSP × H.264/RTP 示例(要点)

  • SPS/PPS 发送:常在关键帧前发送(或复写)以便随时解码入会。
  • FU‑A 分片:大于 MTU 的 IDR/P 片会被拆分;
    • FU-Indicator = F|NRI|28
    • FU-Header = S/E 标志 + 原始 NAL Type
    • 只在帧最后一片Marker=1
  • Wireshark 过滤rtsptcp.port==554rtp
  • 识别 H.264 NAL 类型
    • 单包:载荷首字节 F|NRI|TypeType=7(SPS),8(PPS),5(IDR),1(P/B) 等;
    • FU‑A:载荷首两字节:[0]=FU-Indicator(type=28)[1]=FU-Header(S/E/Type)

9) 附:强制关键帧(FFmpeg/libx264 小贴士)

  • 固定 GOPctx->gop_size = FPS;(例如 25 → 每 1s 关键帧)。
  • x264 参数(降低不确定性):
    av_opt_set(ctx->priv_data, "x264-params", "keyint=25:min-keyint=25:scenecut=0", 0);
  • 驱动 I 帧:部分编码器支持将 AVFrame->pict_type=AV_PICTURE_TYPE_I 作为“请求”,但是否立即生效取决于编码器;更可靠还是上面的 keyint/min-keyint/scenecut

10) 术语速记

  • CSeq:RTSP 会话内请求序号。
  • Session:SETUP 响应分配的会话 ID,后续请求要带。
  • SSRC:RTP 同步源 ID,用于区分与同步流。
  • PT:RTP 负载类型;H.264 常用 96(动态,SDP 中声明)。
  • RTP 时间戳:单位 90 kHz;逐帧递增(例如 25 fps → 每帧 +3600)。

参考抓包操作

  1. 过滤:tcp.port == 554 看 RTSP 文本;rtp 看媒体包。
  2. RTP over TCP:展开 RTSP 流中的 Interleaved Data,RTP 解码同样可用。
  3. 典型问题定位:
    • SDP 是否含 packetization-mode=1sprop-parameter-sets
    • 检查 Marker 是否出现在每帧最后一个包;
    • FU‑A 的 S/E 是否成对出现,从 S=1E=1 闭合。
http://www.dtcms.com/a/557513.html

相关文章:

  • 包装设计的网站wordpress禁用导航栏代码
  • 非洲秃鹫优化算法(AVOA)的详细原理和数学公式
  • 怎样建立网站卖东西百度打开百度搜索
  • 淮安网站优化营销案例分享
  • 高效简便的网站开发网站服务器迁移
  • html5与android之间相互调用
  • 用一份 YAML 编排实时数据集成Flink CDC 工程实践
  • 全志SPI-NG框架使用说明
  • 域名及网站建设实训wordpress 不能自定义主题
  • 新河网站网站后台默认用户名
  • 第十二章:终极叩问:我是谁,我往何方?(1)
  • JAVA高频面试题
  • 如何制作一个自己的网站?安全教育平台登录入口网址
  • 软考 系统架构设计师系列知识点之杂项集萃(184)
  • Redis性能提升秘籍:大Key与热点Key优化实战
  • 大专物流管理专业职业发展指南
  • 徐州网站制作机构做猎头需要用到的网站
  • 石家庄做网站制作公司做公司点评的网站
  • Git指令集
  • 基于边缘信息提取的遥感图像开放集飞机检测方法
  • 前端基础知识---Promise
  • Java 基础——函数式编程
  • webkitx(Android WebView 最佳实践库)
  • 调查网站做调查不容易过横栏建设网站
  • 勐海县住房和城乡建设局网站南昌做网站费用
  • 感知上下文并可解释地预测合成致死药物靶点的大语言模型研究
  • AI研究-117 特斯拉 FSD 视觉解析:多摄像头 - 3D占用网络 - 车机渲染,盲区与低速复杂路况安全指南
  • 二级域名可以做网站吗免费个人博客网站模板下载
  • 复原大唐3d项目测试版
  • 2024年MySQL 下载、安装及启动停止教程(非常