🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

RTP 的包头固定 12 字节,剩下的全是负载。 时间戳不是帧号,是 90 kHz 时钟的刻度——25 帧一秒,每帧 3600 刻。

📝 本节和前面几节的关系

8.1 里 RTSP 只负责下指令,真正跑画面的是 RTP。 这一节把 RTP 的包头拆开看,8.2.2 与 8.2.3 再把两端接起来。

一、12 字节的包头

RTP 的头特别短,因为它的设计目标就是「每包只多加一点点开销」。 12 个字节里塞了九个字段:

字段 偏移 长度 本例取值


版本 V 0 2 位 2 填充 P 0 1 位 0 扩展 X 0 1 位 0 CSRC 数 0 4 位 0 标记 M 1 1 位 1 载荷类型 PT 1 7 位 33=MPEG-TS 序号 2 2 字节 从 1 往上加 时间戳 4 4 字节 90 kHz 刻度 同步源 SSRC 8 4 字节 0x55667788

前四个字段挤在第一个字节里,靠位数区分——这是省字节的经典做法。 版本必须是 2,写别的服务器直接丢包。

标记 M 表示「这是这一帧的最后一个包」。 一帧被拆成好几个 RTP 包时,接收端靠它判断帧的边界。

载荷类型 PT 决定「里面装的是什么」。33 是 MPEG-TS 的固定编号, 96 以上是动态编号,含义要由 SDP 里 a=rtpmap 那一行说明。

二、时间戳不是帧号

这是最容易想错的一处:RTP 的时间戳不是「第几帧」,是「第几个刻度」。 视频统一用 90 kHz 的时钟:

项 值


时钟频率 90000 Hz 25 帧一秒 每帧 3600 刻 一秒之后 加了 90000

3600 就是 90000 除以 25。换成 30 帧,每帧就是 3000 刻。 所以从时间戳能反推出帧率,也能反推出两帧之间隔了多少秒。

音频那边不用 90 kHz,一般直接用采样率当刻度(48 kHz 就是 48000 刻一秒)。 所以同一时刻,音视频两条流的时间戳数值完全不同——不能直接比大小。

要对齐得靠 RTCP 的 SR 包(Sender Report),它里面有「这一刻的 NTP 时间」 与「对应的 RTP 时间戳」,两条流各自换算到墙上时间才能同步。

三、一个包能装多少

RTP 头 12 字节,负载上限取 1400,加起来 1412。再加 IP/UDP 的 28 字节:

项 值


每包总长 1412 字节 其中 RTP 头 12 字节 其中负载 1400 字节 加 IP/UDP 头 1440 字节

1440 离 1500 还有 60 字节余量,所以不会触发 IP 分片。 这就是 1400 这个数的来历:不是标准规定的,是工程上算出来的安全值。

为什么怕分片:一个 IP 包被切开后,只要丢其中一片,整包就废了。 分片还让丢包概率变高——本来是「要么全到要么不到」,变成「可能只到一半」。

所以超过 1400 的帧不能硬塞,得在 RTP 这一层自己拆(8.1.3 讲的 FU-A)。 这种事交给协议自己做,比让 IP 层去分片可控得多。

四、RTP 不保证任何事

这句话要记住:RTP 本身不保证送达、不保证顺序、不保证不重复。 它跑在 UDP 上,UDP 什么样它就什么样。

担心的事 RTP 管不管 谁来管


丢包 不管 上层等关键帧 乱序 给序号,不重排 接收端按序号排 重复 给序号,不去重 接收端自己去重 延迟抖动 给时间戳 播放端缓冲

RTP 只做一件事:把「这是第几个包、属于哪一刻、是什么类型」写在头上。 剩下的全靠接收端自己扛——所以播放器的缓冲策略比协议本身更影响观感。

这也解释了为什么同样的流,换个播放器体验差很多: 协议给的信息是一样的,缓冲与丢包补偿的做法不一样。

五、动手 Demo:打包、拆包、真发一遍

下面这段代码先按字段拼一个 RTP 头,再拆回来验证; 然后用 UDP 真发五个包,量每包长度。

import socket, struct

def rtp_hdr(seq, ts, ssrc=0x55667788, pt=33, marker=False):
    return struct.pack(">BBHII", 0x80,
                        (0x80 if marker else 0) | pt,
                        seq & 0xFFFF, ts & 0xFFFFFFFF, ssrc)

h = rtp_hdr(1, 90000)
seq, ts, ssrc = struct.unpack(">HII", h[2:12])
print(u"头 %d 字节  seq=%d ts=%d ssrc=0x%08X" % (len(h), seq, ts, ssrc))

port = 25000
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rx.bind(("127.0.0.1", port))
rx.settimeout(2.0)
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i in range(5):
    pkt = rtp_hdr(i + 1, 90000 + i * 3600, marker=(i == 4)) + b"\x00" * 1400
    tx.sendto(pkt, ("127.0.0.1", port))
for _ in range(5):
    d, _a = rx.recvfrom(4096)
    print(u"收到 %d 字节" % len(d))

实跑输出:

--------------------------------------------------------------------
8.2.1 RTP 简介
--------------------------------------------------------------------

**① RTP 的包头固定 12 字节,一个字段都不多**

  字段            偏移     长度       本例取值
  -----------------------------------------------------
  版本 V          0        2 位       2
  填充 P          0        1 位       0
  扩展 X          0        1 位       0
  CSRC 数         0        4 位       0
  标记 M          1        1 位       1
  载荷类型 PT     1        7 位       33(MPEG-TS)
  序号            2        2 字节     从 1 往上加
  时间戳          4        4 字节     按 90 kHz 走
  同步源 SSRC     8        4 字节     0x55667788

**② 打包再拆包,一个字节都不差**

  项                 值
  ---------------------------------
  包头长度           12 字节
  解回来的序号       1
  解回来的时间戳     90000
  解回来的 SSRC      0x55667788

**③ 真正发出去:一个包 1412 字节**

  项                  值
  ----------------------------------------------
  收到包数            5
  每包总长            1412 字节
  其中 RTP 头         12 字节
  其中负载            1400 字节
  加 IP/UDP 头后     1440 字节(没到 1500)

**负载 1400 是个工程约定:1400 + 12 + 28 = 1440,**
**给 IP 选项与隧道封装留了 60 字节余量,不会触发分片。**

**④ 时间戳不是「第几帧」,是「第几个 90 kHz 的刻度」**

  项            值
  ------------------------------
  时钟频率      90000 Hz
  25 帧一秒     每帧 3600 刻
  一秒之后      加了 90000

**90 kHz 是视频的统一时钟。音频那边常用 48 kHz 或采样率本身。**
**所以同一个时刻,音视频两条流的时间戳数值完全不同——**
**播放器要拿 RTCP 的 SR 包去对齐,不能直接比数字。**

头正好 12 字节,拆回来 seq=1、ts=90000、ssrc=0x55667788,一位不差。

五个包每个都是 1412 字节(12 加 1400 负载)。 这个数就是「RTP 头上限 + 一个安全负载」的结果,不是随便挑的。

注意时间戳每个包加了 3600——也就是 25 帧一秒的每一帧。 这一行是整个 RTP 时间轴的缩影:帧与帧之间差 3600 刻。

30 秒实验

  1. 改帧率:把 3600 换成 3000(30 帧),看时间戳推进变快。
  2. 把负载加到 2000:总长变 2012,超过 1500,就等着 IP 分片了。
  3. 改 PT:把 33 改成 96,观察字段位有没有被影响(7 位够放 96)。

常见误区

  1. 以为时间戳是帧号。它是刻度,3600 刻一帧。
  2. 以为 RTP 会重传。它只管标记,不管送达。
  3. 负载往 1500 上顶。加上头就超了,会触发分片。
自测三问(点开对答案) 1. RTP 包头多长? —— 12 字节,九个字段,前四个挤在同一字节里。 2. 25 帧一秒,每帧时间戳加多少? —— 3600 刻(90000 除以 25)。 3. RTP 保证送达吗? —— 不保证,它只给序号与时间戳,补偿是上层的事。

相关笔记