📹 第八章 ONVIF框架代码案例应用
📄 一句话概括

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 秒实验

  1. 把 inner 里的 RTP-Unicast 改成 RTP-Multicast,看返回的地址变不变(本机不变)。
  2. 把 timeout 改成 0.05,看错误类型变成什么。
  3. 用 ffprobe 或 PyAV 试着打开那个 URI,看报什么错。

常见误区

  1. 以为 ONVIF 能「取出流」。 它只给地址,取流是 RTSP 和 FFmpeg 的事。
  2. 把 ONVIF 的鉴权和 RTSP 的鉴权当成一套。 前者是 WS-UsernameToken,后者是 HTTP Digest, 密码可能分开配。
  3. 觉得「首帧慢」是代码写得不好。 大部分时间在等第一个 I 帧,GOP 越长等得越久, 只能调设备的 GovLength。
自测三问(点开对答案) 1. `GetStreamUri` 之后 ONVIF 还管什么? —— **什么都不管了。** 它只给一个 URI,**握手、取流、解码都是 RTSP 与 FFmpeg 的事。** 2. 为什么监控场景常用 `-rtsp_transport tcp`? —— **因为 UDP 丢包就花屏**,TCP 会重传。**代价是延迟高一点,但监控更看重稳定。** 3. 「连不上」和「连接被拒」有什么区别? —— **被拒=有主机但端口没开;超时=包发出去没人回**(通常被防火墙丢了)。排查方向完全不同。

下一节抓拍:不用取流也能拿到一张图,而且只要一次 HTTP GET。

相关笔记