JRTPLIB 的用法永远是五步:建会话、设参数、加目的、发、收。 三个新手必错点全在细节里:不释放包、时间戳单位、只调一次 PollData。
3.4.1 把库编出来了,这一节把它用起来。 后面 3.4.5 要发 H.264 码流,用的就是这五步骨架。
一、五步骨架
不管做什么,JRTPLIB 的调用顺序都是这五步:
① 建对象:RTPSession sess;——这一步还没占端口。
② 建会话:sess.Create(params)——bind 端口,定 MTU 与时间戳单位。
③ 设默认值:SetDefaultPayloadType(96)、SetDefaultMark(false)。这两行决定每包头上的 PT 与 M 位。
④ 加目的地址:AddDestination(addr, port)——发的时候用。
⑤ 发与收:SendPacket 发,PollData + GetNextPacket 收。
五步里有四步都是配置,只有第⑤步真干活。 这也是封装型库的标准形态:把「状态」收进对象,把「动作」压成两三个函数。
为什么第①步不占端口?
因为 RTPSession 的构造函数不做任何系统调用,真正的 socket 是在 Create 里建的。
这个设计是为了「先构造后配置」——你可以先把参数准备好再一次建起来。
二、发与收是两个方向
SendPacket 是主动的,调一次发一包。
PollData 是被动的,它只是「去系统缓冲区看看有没有新包」。
所以收包必须写成循环:
① PollData():把网卡上到的包搬进库的内部队列。
② 遍历源:GotoFirstSource / GotoNextSource。
③ 取包:GetNextPacket 取出一个,用完 DeletePacket。
④ 回到①:直到 PollData 返回 0 为止。
这个结构决定了线程模型:
发包在一个线程、收包在另一个线程,或者用库自带的后台收包线程(RTPSessionParams::SetPollThreadActive)。单线程「发完再收」在真实场景里会卡死,因为对端也在等你发。
三、三个必错点
① 收完不释放包。
GetNextPacket 返回的是堆上的对象,用完必须 DeletePacket。
漏一行就稳定泄漏——而这一行恰好是官方示例里最容易被跳过的。
② 时间戳单位没设。
RTPSessionParams 里有个 SetTimestampUnit(1.0 / 90000.0)。
忘了设,默认按「1 秒一个单位」算,时间戳会差出 90000 倍。
现象极难查:收包计数完全正常,但画面一帧都放不出来。
③ 只调一次 PollData。
它一次可能只搬进来几个包,剩下的还在系统缓冲区。
正确写法是一直循环到它返回 0。
少写这个循环的表现是「能收到包,但总比实际少」——丢包率统计会莫名其妙偏高。
三个错误的共同点:都不报错。 JRTPLIB 的接口几乎不抛异常,参数错了就是「行为不对」,所以调试全靠对照现象。
四、一次会话的完整生命周期
按时间顺序看一遍,就知道每一步该放哪:
① 准备:定本地端口、对端地址、PT、时钟频率、MTU。
② 建立:Create 一次,之后所有包共用这个会话。
③ 运行:发线程按帧节奏调 SendPacket,收线程循环 PollData。
④ 反馈:库内部会自动发 RTCP 的 SR 与 RR,不用你管——这是它比自己写省事的又一地方。
⑤ 结束:BYEDestroy 发一个 BYE 再销毁。
直接 delete 也行,只是对端要等超时才知你走了。
注意第④步:RTCP 是自动的。 很多人以为要自己拼 RR 包,其实库默认就开了,只有要读接收端的丢包统计时才需要自己处理 RTCP 回调。
五、动手 Demo:搭一个会话——30 个包,边收边排
照五步骨架跑一遍:一端发 30 个包,另一端按序号排好。 关键看时间戳序列:每三个包一组,同组时间戳相同。
import socket, struct
# ② Create:两端各 bind 一个端口
snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
snd.bind(("127.0.0.1", 0))
rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rcv.bind(("127.0.0.1", 0))
rcv.settimeout(2.0)
dest = rcv.getsockname() # ④ AddDestination
N = 30
for i in range(N): # ⑤ SendPacket
# 每 3 个包算一帧,同帧时间戳相同
hdr = struct.pack("!BBHII", 0x80, 96, 5000 + i, (i // 3) * 3600, 0x1A2B3C4D)
snd.sendto(hdr + bytes([0x41]) + b"p" * 60, dest)
buf = {}
while len(buf) < N: # ⑤ PollData 循环
try:
d, _ = rcv.recvfrom(2048)
except socket.timeout:
break
b0, b1, seq, ts, ssrc = struct.unpack("!BBHII", d[:12])
buf[seq] = (ts, len(d))
seqs = sorted(buf)
print("收到 %d 个包,序号 %d ~ %d" % (len(seqs), seqs[0], seqs[-1]))
print("时间戳前 6 个:%s" % [buf[s][0] for s in seqs[:6]])
print("平均每包 %d 字节" % (sum(v[1] for v in buf.values()) // len(buf)))
实跑输出:
--------------------------------------------------------------------
搭一个会话——收发包的骨架只有五步
--------------------------------------------------------------------
**① 真跑一遍:一端发 30 个包,另一端边收边排**
收到 30 个包,序号 5000 ~ 5029
按序号排好后,时间戳序列的前 6 个:0、0、0、3600、3600、3600
平均每包 73 字节(含 12 字节 RTP 头)
**② 五步骨架,逐句对应**
步 JRTPLIB 里的写法 这一步真正在做什么
------------------------------------------------------------------------
1 RTPSession sess; 建对象,还没占端口
2 Create(RTPSessionParams) bind 端口,定 MTU 与时间戳单位
3 SetDefaultPayloadType(96) 把 PT 写进每个发出的包头
4 AddDestination(addr, port) 记下对端,发的时候用
5 SendPacket / PollData 发与收,收完要释放包
**③ 三个新手最容易错的地方**
**① 收完不释放包。**
**JRTPLIB 的 `GetNextPacket` 返回的是堆上的对象,用完必须 `DeletePacket`。**
**漏一行就稳定泄漏,跑几小时内存爆掉——而这是官方示例里最容易被跳过的一行。**
**② 时间戳单位没设。**
**构造 `RTPSessionParams` 时有个 `SetTimestampUnit(1/90000.0)`。**
**忘了设它就默认按 1 秒算,时间戳会差出 90000 倍。**
**现象是画面一帧都放不出来,但收包计数完全正常——很难查。**
**③ 只调一次 PollData。**
**接收是个循环:`PollData()` → 遍历源 → 取包。只调一次只能拿到一个包,剩下的堆在系统缓冲区里。**
**正确的写法是一个 while 循环,直到取不出为止。**
30 个包全部收到,序号 5000 到 5029——沙箱环回上 UDP 几乎不丢,真实网络不会这么干净。 时间戳序列是 0、0、0、3600、3600、3600——三个包一帧,这就是「序号按包、时间戳按帧」的直接证据。 平均 73 字节 = 12 字节头 + 1 字节 NAL 头 + 60 字节载荷,头占了六分之一——小包传输时开销比例非常难看。
30 秒实验
- 把
i // 3改成i,看时间戳序列变成什么——变成「一包一帧」,同帧共享时间戳的关系就消失了。 - 把 30 个包里的
seq打乱顺序再发,看排序逻辑还对不对——这就是PollData内部排队窗口在做的事。 - 把
rcv的settimeout改成 0.01 秒,看会不会收不全——超时太短会在「包还没搬完」时就退出循环。
常见误区
- 「GetNextPacket 拿到的包不用管」——必须
DeletePacket,否则稳定泄漏。 - 「时间戳单位用默认的就行」——默认是秒,视频要设成 1/90000,否则差 90000 倍。
- 「PollData 调一次就够」——要循环到返回 0,否则收包数总比实际少。
自测三问(点开对答案)
1. 五步骨架里,哪一步才真正占端口? —— **第②步 `Create`。**构造函数不碰系统调用。 2. 为什么收包必须写成循环? —— **一次 `PollData` 只搬一部分包。**剩下的还在系统缓冲区里,不循环就会「收得到但收不全」。 3. RTCP 的 SR、RR 要自己发吗? —— **不用,库默认自动发。**只有要读丢包统计时才处理 RTCP 回调。- ☐ (本节完,下一节 3.4.4 RTP与H.264的相关结构体)