
目标:理解 RTSP / RTP / RTCP 的分工,跑通 摄像头 → RTSP Server → 客户端 链路,并让云服务器也能拉到家里树莓派的实时画面。
环境:树莓派(aarch64,IMX219 摄像头,硬件 H.264 编码)、MediaMTX v1.21.0、Mac(FFmpeg 8.1 / Wireshark)、一台腾讯云 Ubuntu 服务器。
一、先搞清楚:三个协议各管什么
编码后的 H.264 码流只是一串 NAL 单元,自己不具备"网络传输"能力。要实时送到远端,要解决三件事:
| 协议 | 角色 | 一句话类比 |
|---|---|---|
| RTSP | 控制信令:描述流、协商传输方式、播放/停止 | 遥控器 |
| RTP | 媒体数据:带序号和时间戳的分片包 | 传送带上的包裹 |
| RTCP | 质量反馈:丢包、抖动、时间同步 | 签收回执 |
关键认识:RTSP 本身不传视频,只负责"商量怎么传",视频走 RTP。RTSP 的文本格式很像 HTTP(请求 / 响应 / 状态码),抓包时读起来很亲切。
握手流程:OPTIONS → DESCRIBE → SETUP → PLAY,结束时 TEARDOWN。
二、搭建链路
Camera → 硬件 H.264 编码 → MediaMTX (RTSP Server) ──RTP──→ ffplay / ffmpeg / VLC
rtsp://<PI_IP>:8554/cam ←─RTSP──
选 MediaMTX 的原因:单个二进制、零依赖,并且原生支持树莓派摄像头(source: rpiCamera)。
mediamtx.yml:
yaml
paths:
cam:
source: rpiCamera
rpiCameraWidth: 1280
rpiCameraHeight: 720
rpiCameraFPS: 30
rpiCameraBitrate: 2000000 # 2 Mbps
rpiCameraIDRPeriod: 50 # 关键帧间隔(GOP),30fps 下约 1.67 秒
启动后日志里出现下面两行才算成功:
[RTSP] started with listeners on :8554 (TCP/RTSP), :8000 (UDP/RTP), :8001 (UDP/RTCP)
[path cam] stream is available and online, 1 track (H264)
Mac 上验证:
bash
ffprobe -rtsp_transport tcp rtsp://<PI_IP>:8554/cam
ffplay -rtsp_transport tcp -fflags nobuffer -flags low_delay rtsp://<PI_IP>:8554/cam
ffplay -rtsp_transport udp -fflags nobuffer -flags low_delay rtsp://<PI_IP>:8554/cam
输出 Video: h264 (Main), yuv420p, 1280x720, 30 fps,与配置一致。
三、抓包观察:把协议"看见"
用 tcpdump 在 Mac 上分别抓 TCP 和 UDP 两次,再用 Wireshark 分析:
bash
sudo tcpdump -i en0 -w rtsp_udp.pcap host <PI_IP>
3.1 SETUP 是 TCP 和 UDP 唯一的区别
四步握手里,OPTIONS / DESCRIBE / PLAY 两种模式完全一样,差异只在 SETUP 的 Transport 头:
| TCP interleaved | UDP | |
|---|---|---|
| 请求 | RTP/AVP/TCP;unicast;interleaved=0-1 |
RTP/AVP/UDP;unicast;client_port=8494-8495 |
| 响应 | ...interleaved=0-1;ssrc=E6B2D088 |
...client_port=8494-8495;server_port=8000-8001;ssrc=E6B2D088 |
| 媒体走哪里 | 复用 8554 的 TCP 连接,$ 开头的 interleaved 帧 |
独立 UDP 端口对,偶数 RTP、奇数 RTCP |
3.2 DESCRIBE 返回的 SDP:播放器怎么知道如何解码
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=<SPS>,<PPS>
H264/90000:H.264,RTP 时钟 90 kHzpacketization-mode=1:支持 FU-A 分片profile-level-id=4D4029:4D= Main profile,29(十六进制)= 41,即 Level 4.1,与 ffprobe 看到的h264 (Main)一致sprop-parameter-sets:逗号前是 SPS、后是 PPS(base64),解码器初始化需要
3.3 RTP 包里能读出什么
在 rtsp_udp.pcap 里过滤 rtp:
- Payload type = 96 ,对应 SDP 的
RTP/AVP 96 - SSRC = 0xE6B2D088 ,与 SETUP 响应里的
ssrc一致 - Sequence number 从 54419 开始连续递增,没有跳号 → 这段没丢包。序号用来检测丢包和乱序
- Timestamp :相邻帧差 3000,
90000 ÷ 3000 = 30,正好是 30 fps。时间戳决定"这一帧什么时候播",也用于计算抖动 - 同一帧共用一个时间戳 ,最后一个包带
Marker - 分片 :满载的包长度 1494(被 MTU 限制切满的分片;SDP 是
packetization-mode=1,所以推断为 FU-A,我没有逐包解析 payload 确认),最后一个包长度不定(520、733、640),装剩余部分。"一帧超过 MTU 要拆包"在这里就是实物 - 同一帧内的包间隔仅微秒级,是突发发出的,帧间隔约 33 ms。这种突发在弱网下容易丢包
3.4 RTCP:质量回执
rtcp 过滤后看到 1 个 Sender Report(树莓派发)和约每秒 1 个 Receiver Report(Mac 发)。
Sender Report:
| 字段 | 值 | 含义 |
|---|---|---|
| 端口 | 8001 → 8495 | 与 SETUP 协商的 server_port / client_port 吻合 |
| NTP timestamp ↔ RTP timestamp | 成对出现 | 把 RTP 时钟刻度映射到真实时间,多路流同步靠它 |
| Sender's packet count / octet count | 67 / 85835 | 到该时刻已发包数和字节数 |
Receiver Report:
| 字段 | 值 |
|---|---|
| Fraction lost | 0 / 256 |
| Cumulative number of packets lost | 0 |
| Extended highest sequence number received | 55048 |
| Interarrival jitter | 54(RTP 刻度)→ 54 ÷ 90 ≈ 0.6 ms |
| Delay since last SR | 191790(约 2926 ms) |
自洽性检查:起点 54419、最大序号 55048,共 630 个包、累计丢包 0。
小坑:RR 的
Sender SSRC(...089)是接收端 ffmpeg 给自己生成的,被报告的流是Source 1里的 Identifier(...088),别混淆。
四、UDP vs TCP 实测对比
分辨率 / FPS / 码率是树莓派编码器决定的,UDP 和 TCP 只是传输方式,视频本身两边相同。真正有差异的是线路开销和异常场景下的表现。
录 10 秒(-c copy),再用 ffprobe 读真实值:
bash
ffprobe -v error -select_streams v:0 -count_frames \
-show_entries stream=nb_read_frames:format=duration,size,bit_rate -of default=nw=1 x.mp4
| 项目 | UDP | TCP |
|---|---|---|
| 分辨率 / FPS | 1280x720 / 30.0(275 帧 ÷ 9.16 s) | 1280x720 / 30.0(262 帧 ÷ 8.73 s) |
| 实测码率 | 2.00 Mbps | 2.03 Mbps |
| 丢包 | 0(RTCP 0/256) | TCP 重传,应用层无丢失 |
| 抖动 | 约 0.6 ms | 未测 |
| 线路码率(含包头) | 约 2.1 Mbps,约 200 包/秒 | 约 2.2 Mbps,约 300 包/秒 |
| mediamtx CPU | 约 3% | 约 3% |
| 首帧时间(5 次平均,含 ffmpeg 启动与探测) | 1.65 s | 1.63 s |
解读:
- 线路码率比视频码率高约 5%~10% :多出来的是以太网 / IP / UDP / RTP(或 TCP)头。抓包里的
SUM(frame.len)单位是字节,要乘 8。 - TCP 每秒包数更多:Mac 回的 ACK 小包也被抓进来了,字节数却差不多。
- mediamtx 进程的 CPU 只有约 3% :它只做包转发、不转码。注意这只是 mediamtx 这一个进程的数据:摄像头采集和硬件编码由它拉起的子进程负责,整机负载这次没有测(无人拉流时整机
top显示us约 23%,没有拆分是哪些进程占的)。与软编码(libx264)的对比这次也没有实测,留到后面补。 - 局域网下 UDP 与 TCP 在 FPS、码率、首帧时间上都没有可区分的差异。 协议差异在弱网和跨网络时才会显现,这是 Day 11 的实验。
五、让云服务器也拉到流:SSH 反向隧道
树莓派在家庭 NAT 后面,云服务器无法主动连进来。今天用最快的方案:在树莓派上建反向隧道,把云服务器本地的 8554 映射回树莓派的 8554:
bash
ssh -N -R 127.0.0.1:8554:127.0.0.1:8554 <user>@<CLOUD_IP>
然后在云服务器上拉流(必须 TCP):
bash
ffprobe -rtsp_transport tcp rtsp://127.0.0.1:8554/cam
ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1:8554/cam -t 10 -c copy cloud_test.mp4
结果:ffprobe 拿到 h264 (Main) 1280x720 30 fps;录制 294 帧、约 2.4 MB,与树莓派发布的参数一致。隧道只绑定 127.0.0.1,不会把摄像头暴露到公网,云服务器安全组也不需要放行 8554。
对照实验:同样的隧道,换成 UDP
bash
ffprobe -rtsp_transport udp rtsp://127.0.0.1:8554/cam
Failed reading RTSP data: End of file
Could not find codec parameters for stream 0 (Video: h264, none): unspecified size
Stream #0:0: Video: h264, none, 90k tbr, 90k tbn
原因:OPTIONS / DESCRIBE 走 8554/TCP,隧道能转发,所以 ffprobe 知道"有一路 h264"。但 SETUP 协商成 UDP 后,服务端把 RTP 往云服务器的 UDP 端口发,而隧道只转发 TCP,一个媒体包都收不到,于是拿不到 SPS/PPS 和分辨率。
这是"RTSP 信令通 ≠ 视频通"的活证据,也解释了为什么文档说"跨公网 / 云服务器一律先用 TCP"。
六、踩坑记录
坑 1:ffprobe 返回 404 Not Found
现象 :./mediamtx 启动后立即退出,日志里有 ERR listen udp :8000: bind: address already in use;随后 ffprobe 报 404 Not Found。
排查:
bash
sudo ss -lunp | grep -E ':8000|:8001'
sudo ss -lntp | grep 8554
ps aux | grep mediamtx
发现 pid 1518 是以 root 运行的另一个 MediaMTX(/opt/mediamtx/mediamtx /opt/mediamtx/mediamtx.yml),由 systemd 开机自启,同时占着 8000 / 8001 / 8554。
根因 :手动启动的实例因为 8000 端口冲突退出;ffprobe 实际连到的是后台那个实例,它的配置里没有 cam 路径,所以 404。404 是后果,不是根因。(说明:我没有 cat 过 /opt/mediamtx/mediamtx.yml,"里面没有 cam"是根据 404 和端口归属反推的。)
解决:
bash
sudo systemctl stop mediamtx
sudo systemctl disable mediamtx # 不想开机自启就加
经验:先看日志里的第一个 ERR,别被最后的现象带偏;ss -lntp/-lunp 能直接看到谁占了端口。
坑 2:ssh -N 输完密码"卡住",随手 Ctrl+C
-N 表示只建隧道、不开 shell,输入密码后终端没有任何输出是正常的 ,说明隧道已经建好。按 Ctrl+C 隧道就断了。云服务器上也要先 sudo apt install ffmpeg 才有 ffprobe。
坑 3:Wireshark 菜单里找不到 Telephony
Mac 版的菜单栏找不到时用 tshark 同样能拿到数据:
bash
tshark -r rtsp_udp.pcap -q -z rtp,streams
tshark -r rtsp_udp.pcap -q -z io,stat,1,"COUNT(frame)frame","SUM(frame.len)frame.len"
坑 4:测量口径的几个陷阱
- ffmpeg
-c copy录制打印的码率偏低 :录制会丢弃第一个关键帧之前的帧,而 ffmpeg 仍以挂钟时间(10.06 s)作分母;用 ffprobe 读成品文件的bit_rate才准(约 2.0 Mbps,而不是 1757 kbit/s)。这是我基于帧数反推的解释:丢掉的开头帧数与 GOP 位置吻合,但没有逐帧验证。 top -b -n 1的单次采样没意义 :第一次输出是从进程启动起算的平均值,且当时没有客户端在拉流(显示 0.0%)。应该先持续拉流,再top -b -d 1 -n 6,忽略第一行取后几行平均。- 单次测首帧时间会被误导:第一次测得 "TCP 2.6 s / UDP 1.5 s",交替跑 5 轮后变成 1.63 s vs 1.65 s。单次样本混入了冷启动和 GOP 位置的随机性。
七、收尾与安全
- MediaMTX 默认 8554 无认证 ,不要直接暴露到公网;需要时配置
authInternalUsers,或继续用隧道 / 内网 - 隧道只绑定 127.0.0.1,云服务器安全组无需放行 8554
- 长期使用建议 SSH 改密钥登录(
ssh-keygen -t ed25519+ssh-copy-id),也方便 Day 10 做自动重连 - 我停用了开机自启的 mediamtx 服务;如果其他程序依赖它,用
sudo systemctl enable --now mediamtx恢复,两个实例只能开一个 - 实验结束后
Ctrl+C停掉 mediamtx 和隧道,把mediamtx.yml与命令存入 git
八、今天的收获
- RTSP / RTP / RTCP 分别是遥控器、传送带、回执,抓一次包胜过看十遍概念
- SSRC、序号、时间戳、Marker、RTCP 的数值互相都能对上:协议不是黑盒
- 视频质量要用统一口径测量,ffmpeg 打印的码率、
top单次采样、单次首帧耗时都可能误导 - 排查问题先看第一个报错,再看现象;404 的根因是端口冲突,不是路径写错
- 链路已打通:摄像头 → 硬件 H.264 → MediaMTX → RTSP/TCP → SSH 隧道 → 云服务器录成 MP4
下一站 Day 10:让树莓派主动推流到云端 MediaMTX,完成第一次"远程看见摄像头"的完整闭环。