GetStreamUri 是整个 ONVIF 的终点站:它返回一个 RTSP 地址,然后就不管了。
后面的握手、取流、解码、渲染,全是 RTSP 和 FFmpeg 的事。
把这条界线搞清楚,很多「ONVIF 取不到流」的困惑就消失了。
前面几节都在「问设备」,这一节开始「要画面」。 这是本课与 26/30/34 课的交界处: ONVIF 负责「告诉你去哪取」,RTSP 与 FFmpeg 负责「真把数据拿回来」。
一、请求里那个 StreamSetup
| 字段 | 取值 | 含义 |
|---|---|---|
Stream |
RTP-Unicast |
单播,一对一(默认) |
Stream |
RTP-Multicast |
组播,一对多 |
Transport/Protocol |
RTSP |
标准做法 |
Transport/Protocol |
HTTP |
穿防火墙用 |
第一行是绝大多数场景的选择:一个人看一路流,单播最简单。
第二行只在「一台上百人看同一路」时才划算:
组播不复制流,但要求整条链路都支持组播——跨网段、跨运营商基本走不通。
第四行那个 HTTP 容易误解:
它不是「用 HTTP 传视频」,是「用 HTTP 隧道把 RTSP 包起来」——
媒体还是 RTP,只是控制通道走了 HTTP 的 80 端口。
这个选项在「客户端在防火墙后面」时有用,因为很多企业网只放行 80/443。
二、返回的 URI 就是终点
GetStreamUri 的响应里只有一个有用的字段:tt:Uri。
拿到之后,ONVIF 的使命就结束了。
ffmpeg -rtsp_transport tcp -i "rtsp://…/onvif/profile1" \
-c copy out.mp4
这条命令里没有一处和 ONVIF 有关:-rtsp_transport tcp 是 RTSP 的选项,
-c copy 是 FFmpeg 的转封装。
为什么要 -rtsp_transport tcp?
因为默认是 UDP,丢包就花屏;
TCP 会重传,画质稳但延迟高一点——监控场景通常选 TCP,因为「稳定」比「低延迟」重要。
这条界线要划清:「ONVIF 取不到流」这个说法本身就不准确,
准确的说法是「ONVIF 给的地址连不上」或「RTSP 握手失败」——两者排查方向完全不同。
三、为什么「打开预览要一两秒」
实测:本地 mp4 预热后解出首帧不到 10 毫秒,而真实 RTSP 要一两秒。 多出来的时间全在「等」上:
| 阶段 | 耗时量级 | 能不能省 |
|---|---|---|
| TCP 建连 | 几十毫秒 | 能(复用) |
| RTSP 五步握手 | 一到两个往返 | 省不了 |
| 等第一个 I 帧 | 最多一个 GOP | 省不了 |
| 解码首帧 | 十毫秒量级 | 省不了 |
第三行是最大的一块:
如果 GOP 是 50 帧、帧率 25,那就是 2 秒一个关键帧——最坏情况下你要等满 2 秒才能出第一帧。
所以「首帧慢」这件事,本质上是「GOP 太长」,而不是「代码写得慢」。
要改善只能调设备的 GOP 长度(GovLength 那个字段)。
第二行那个「省不了」也有例外:有的设备支持「快速启动」或者「按需 I 帧」,
但那是厂商扩展,不是 ONVIF 标准。
四、取流失败的四种原因
| 现象 | 原因 | 怎么确认 |
|---|---|---|
| 连接被拒 | 554 端口没开 | telnet 设备 554 |
| 连上但立刻断 | 鉴权失败(RTSP 层) | 看 RTSP 的 401 |
| 握手完没数据 | Profile 没配编码 | GetProfiles 看配置 |
| 有数据但解不出 | 编码格式不支持 | 看 SDP 里的 a=rtpmap |
第二行那个「RTSP 层的鉴权」和 ONVIF 的鉴权是两套:
ONVIF 用 WS-UsernameToken,RTSP 用 HTTP Digest——所以「ONVIF 能连、流取不到」是很常见的,
因为两边的密码可能不一样(有的设备分开配)。
第三行在「设备刚恢复出厂」时特别常见:Profile 存在,但编码参数是空的,
GetStreamUri 照样返回一个地址,连上去没数据。
第四行最麻烦:
设备用 H.265,而你的 FFmpeg 没编进 H.265 解码器——
表现是「有数据、解不出帧」,日志里只有一句 decoder not found。
五、动手 Demo:ONVIF 只负责给地址,不负责给画面
Demo 先拿地址,再真去连 554 端口(必然失败,本机没有 RTSP 服务), 然后用一个本地 mp4 当替身,量「拿到地址之后」的时间。 这一段的价值是「把界线划清」。
# ① 拿地址
inner = ("<trt:GetStreamUri><trt:StreamSetup>"
"<tt:Stream>RTP-Unicast</tt:Stream>"
"<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>"
"</trt:StreamSetup>"
"<trt:ProfileToken>Profile_1</trt:ProfileToken></trt:GetStreamUri>")
_a, b, _n = S.call(port, "media", "GetStreamUri", inner)
uri = re.search(r"<tt:Uri>([^<]+)</tt:Uri>", b).group(1)
# ② 真去连 554(本机必然拒绝/超时)
try:
socket.create_connection(("127.0.0.1", 554), timeout=0.5)
except OSError as e:
print("连不上:", e)
实跑输出:
--------------------------------------------------------------------
ONVIF 只负责给地址,不负责给画面
--------------------------------------------------------------------
**这是第八章最需要摆正的一个认识**:
**`GetStreamUri` 返回一个 RTSP 地址,然后就结束了。**
**后面的取流、解码、显示全是 FFmpeg 和 RTSP 的事,跟 ONVIF 无关。**
拿到的地址:`rtsp://127.0.0.1:554/onvif/profile1`
**这个字符串就是终点。**
**把它交给 FFmpeg,剩下的事和 ONVIF 一点关系都没有**:
`ffmpeg -rtsp_transport tcp -i "rtsp://127.0.0.1:554/onvif/profile1" -c copy out.mp4`
**我按这个地址去连 554 端口**:**连不上:TimeoutError**(507.3 毫秒)
**这个失败是必然的**:地址是设备「声称」的,
**本机根本没有这个 RTSP 服务**——
**ONVIF 返回地址不代表地址上的服务真在跑。**
**为了看清「拿到地址之后」是什么样,我用一个本地 mp4 当替身**:
预热一次之后,打开文件 → 解出第一帧:4.2 毫秒
首帧尺寸:320 × 240,编码 h264,帧率 30
**这个时间放在网络上要乘以好几倍**,
**因为 RTSP 还要先握手(OPTIONS/DESCRIBE/SETUP/PLAY)**,
**再等第一个 I 帧到齐才解得出来。**
**所以「打开预览要一两秒」是正常的**,
**不是代码慢,是在等关键帧。**
第二段那个 timeout=0.5 是必要的:不设的话,连一个不通的端口会等几十秒(取决于系统)。
输出里那句「连不上:TimeoutError」比「连接被拒」更值得注意:「被拒」说明有主机但端口没开,
「超时」说明包发出去没人回——两者的排查方向完全不同(后者通常是防火墙丢包)。
第三段那个「用 mp4 当替身」是本节唯一能真跑的部分:
它证明「拿到地址之后解码一帧有多快」——不到 10 毫秒。
所以「打开预览要一两秒」的时间全在网络和等关键帧上,
不是解码慢。这个结论只有把两段分开量才能得出。
30 秒实验
- 把
inner里的RTP-Unicast改成RTP-Multicast,看返回的地址变不变(本机不变)。 - 把
timeout改成 0.05,看错误类型变成什么。 - 用
ffprobe或 PyAV 试着打开那个 URI,看报什么错。
常见误区
- 以为 ONVIF 能「取出流」。 它只给地址,取流是 RTSP 和 FFmpeg 的事。
- 把 ONVIF 的鉴权和 RTSP 的鉴权当成一套。 前者是 WS-UsernameToken,后者是 HTTP Digest, 密码可能分开配。
- 觉得「首帧慢」是代码写得不好。 大部分时间在等第一个 I 帧,GOP 越长等得越久,
只能调设备的
GovLength。
自测三问(点开对答案)
1. `GetStreamUri` 之后 ONVIF 还管什么? —— **什么都不管了。** 它只给一个 URI,**握手、取流、解码都是 RTSP 与 FFmpeg 的事。** 2. 为什么监控场景常用 `-rtsp_transport tcp`? —— **因为 UDP 丢包就花屏**,TCP 会重传。**代价是延迟高一点,但监控更看重稳定。** 3. 「连不上」和「连接被拒」有什么区别? —— **被拒=有主机但端口没开;超时=包发出去没人回**(通常被防火墙丢了)。排查方向完全不同。下一节抓拍:不用取流也能拿到一张图,而且只要一次 HTTP GET。
- ☐ (本节完,下一节 08.06 图像抓拍)