智能摄像头系统 30 天学习计划 Day 09:用 RTSP 把视频送到网络

目标:理解 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 kHz
  • packetization-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

解读:

  1. 线路码率比视频码率高约 5%~10% :多出来的是以太网 / IP / UDP / RTP(或 TCP)头。抓包里的 SUM(frame.len) 单位是字节,要乘 8。
  2. TCP 每秒包数更多:Mac 回的 ACK 小包也被抓进来了,字节数却差不多。
  3. mediamtx 进程的 CPU 只有约 3% :它只做包转发、不转码。注意这只是 mediamtx 这一个进程的数据:摄像头采集和硬件编码由它拉起的子进程负责,整机负载这次没有测(无人拉流时整机 top 显示 us 约 23%,没有拆分是哪些进程占的)。与软编码(libx264)的对比这次也没有实测,留到后面补。
  4. 局域网下 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:测量口径的几个陷阱

  1. ffmpeg -c copy 录制打印的码率偏低 :录制会丢弃第一个关键帧之前的帧,而 ffmpeg 仍以挂钟时间(10.06 s)作分母;用 ffprobe 读成品文件的 bit_rate 才准(约 2.0 Mbps,而不是 1757 kbit/s)。这是我基于帧数反推的解释:丢掉的开头帧数与 GOP 位置吻合,但没有逐帧验证。
  2. top -b -n 1 的单次采样没意义 :第一次输出是从进程启动起算的平均值,且当时没有客户端在拉流(显示 0.0%)。应该先持续拉流,再 top -b -d 1 -n 6,忽略第一行取后几行平均。
  3. 单次测首帧时间会被误导:第一次测得 "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

八、今天的收获

  1. RTSP / RTP / RTCP 分别是遥控器、传送带、回执,抓一次包胜过看十遍概念
  2. SSRC、序号、时间戳、Marker、RTCP 的数值互相都能对上:协议不是黑盒
  3. 视频质量要用统一口径测量,ffmpeg 打印的码率、top 单次采样、单次首帧耗时都可能误导
  4. 排查问题先看第一个报错,再看现象;404 的根因是端口冲突,不是路径写错
  5. 链路已打通:摄像头 → 硬件 H.264 → MediaMTX → RTSP/TCP → SSH 隧道 → 云服务器录成 MP4

下一站 Day 10:让树莓派主动推流到云端 MediaMTX,完成第一次"远程看见摄像头"的完整闭环。

相关推荐
小猪佩奇TONY1 小时前
Linux 内核学习(19) --- linux waitqueue
linux·学习·waitqueue
KobeSacre1 小时前
计算机网络概览
网络·计算机网络
m4Rk_1 小时前
【论文阅读】Agent记忆机制(111):FEB-30——别再只用问答准确率评估 Agent Memory
论文阅读·人工智能·学习·开源·github
凉、介1 小时前
深入理解 ARMv7-M | 异常/中断处理
笔记·学习·嵌入式·arm
凉、介1 小时前
深入理解 ARMv7-M|处理器模式与寄存器
笔记·学习·嵌入式·arm
SCandL1523 小时前
RHEL9 配置外网完整流程
网络
奇牙coding4 小时前
doubao-seed-2.1-pro 报 HTTP 401 怎么办?Key 正确也要检查请求头和端点
网络·数据库·http·ai
ESDWAN10 小时前
跨境网络丢包怎么定位?用MTR和Traceroute分析链路问题
网络
宝绿特塑业再生科技10 小时前
改性HDPE再生颗粒在户外家具挤出发泡板材中的色差控制与高品质价值重构
经验分享·科技·学习·生活·1024程序员节·户外家具