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

不封装的代价不是行数多,是「XML 长什么样」这件事散落在每处调用里。 封装对了,八行变四行;封装错了,以后每改一次都要动所有调用方。

📝 本节和前面几节的关系

08.08.09 把命名空间表定下来了,这一节把「怎么用它」收进一个类。 08.03 到 08.07 那五个操作都是各自独立的例子,这一节要把它们串成一个能复用的小客户端。

一、不封装长什么样

取一个流地址,要写这么多:

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)

八行,只换来一个字符串。 如果客户端要调八个操作,这种样板就是八份, 而八份里每一份都有「命名空间写错、元素顺序写反」的机会。 注意最后一行那个正则: 它假设返回里只有一个 <tt:Uri>——GetStreamUri 的返回确实只有一个,但换个操作就不一定了。

二、封装之后

同样三件事,变成三行:

c = OnvifClient(xaddr)
print(c.info())
print(c.profiles())
print(c.stream_uri("Profile_1"))

第一行建对象时就带了地址, 后面每行只说「要什么」,不说「怎么要」——这是封装的判据:调用方代码里不该出现 XML。 第二行那个 info() 背后是两次调用(设备信息 + 能力集),但调用方不需要知道这个。 该合并的合并掉,是封装的价值之一。 第三行那个 profiles() 返回的是结构化的列表, 不是 XML 字符串——这样上层代码才能写 for p in profiles。

三、收益不在行数

八行变四行只是副作用,真正的收益是「三件事被关起来了」:

被关起来的东西 以前在哪
命名空间前缀 每处调用
元素顺序 每处调用
解析路径 每处调用

这三样正好是 ONVIF 最烦人的三点:前缀写错报 unbound、顺序写反设备不认、路径写错取到空值。 把它们关进类里之后, 上层代码只面对「方法名 + 参数 + 返回值」,也就是普通函数的样子。 这条界线值得写下来:封装的目的是把「协议细节」和「业务逻辑」隔开,不是把代码变短。

四、该包什么、不该包什么

这两列的分界决定了封装能不能长期活下去:

该包 不该包
拼信封、解析 XML 重试与超时策略
把 token 当参数传 业务判断
把异常统一成一种 日志内容

右列那三项如果包进去,每改一次都要动封装层,而封装层一动,所有调用方都受影响。 这就是「封装过度」比「不封装」更难维护的原因:不封装至少改起来是局部的。 举例说重试: 取流地址失败了该重试三次还是立刻报错,取决于调用它的场景—— 预览界面要立刻报错,后台巡检可以重试。这种决策不能塞进封装层。 左列那三项则是纯协议细节:换一台设备、换一个 ONVIF 版本,它们要变, 而调用方不该跟着变。

五、一个最小可用的轮廓

封装层不需要大,五个方法就能覆盖第八章全部例子:

方法 背后是什么
info() 设备信息 + 能力
profiles() GetProfiles
stream_uri(t) GetStreamUri
snapshot_uri(t) GetSnapshotUri
set_res(w, h) 改编码配置

第一行那个「+ 能力」是我合并的:单独调 GetCapabilities 的场景很少, 大多是为了知道「这台设备支不支持云台」,所以要一个合并后的结果。 第五行那个 set_res 返回什么值得想一下:08.07 已经证明设备会「静默夹边界」, 所以它应该返回「实际生效的分辨率」,而不是返回「成功」。 这条设计是从实测里长出来的:设 3000×2000 返回成功、回读是 1920×1080, 如果封装层只说「成功」,调用方会一直以为自己设对了。

六、动手 Demo:把八行样板压成四行

Demo 在本机搭的模拟设备上真调一遍,对比「裸调」和「封装后」两种写法。 输出里的四个值都是真跑出来的,包括那个抓拍地址上的端口号。

c = OnvifClient(xaddr)
i = c.info()
print("Manufacturer=%s,Model=%s,Firmware=%s"
      % (i["Manufacturer"], i["Model"], i["FirmwareVersion"]))

for p in c.profiles():
    print("Profile: %s(%dx%d)" % (p["token"], p["w"], p["h"]))

print("stream_uri  →", c.stream_uri("Profile_1"))
print("snapshot_uri→", c.snapshot_uri("Profile_1"))

实跑输出:

--------------------------------------------------------------------
把八行样板压成四行
--------------------------------------------------------------------
**不封装的代码长什么样?**
**每个操作都要拼一遍信封、发一遍请求、解一遍 XML**:

  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)

**8 行,只换来一个字符串。**
**如果客户端要调八个操作,这种样板就是八份。**

**封装之后同样三件事**:

  c = OnvifClient(xaddr)
  print(c.info())
  print(c.profiles())
  print(c.stream_uri("Profile_1"))

**真跑一次,看输出**:

info → Manufacturer=ACME,Model=IPC-2000,FirmwareVersion=5.6.1
profiles → MainStream(1920×1080)
stream_uri → `rtsp://127.0.0.1:554/onvif/profile1`
snapshot_uri → `http://127.0.0.1:52731/onvif/snapshot/profile1`

**8 行变 4 行**,但真正的收益不是行数:
**是把「XML 长什么样」这件事关进了类里面。**
**上层代码从此看不到命名空间、看不到元素顺序、看不到解析路径**——
**这正好是 ONVIF 最烦人的三件事。**

**封装层该包什么、不该包什么,有两条界线**:

  该包                  不该包
  -------------------------------------------------------
  拼信封、解析 XML      **重试与超时策略**(要按场景调)
  把 token 当参数传     **业务判断**(哪路流给谁看)
  把异常统一成一种      **日志内容**(现场要看得懂)

**第二列那三项如果包进去,以后每改一次都要动封装层**,
**而封装层一动,所有调用方都受影响**——
**这就是「封装过度」比「不封装」更难维护的原因。**

输出最后两行的地址端口不一样: 一个 554、一个 52731——因为 RTSP 用标准端口,而抓拍走的是 HTTP 服务的临时端口。 这两个地址都是设备算出来告诉你的,客户端不能自己拼: 拼出来的 rtsp://ip:554/onvif/profile1 在你这台设备上对,换一台就 404。 profiles() 返回的是列表, 所以能直接 for 循环——这是「返回结构化数据」和「返回 XML 字符串」的差别。 注意 stream_uri 的参数是 Profile_1 这个 token:它必须从 profiles() 里拿, 不能写死在代码里(下一节会专门讲这件事的后果)。

30 秒实验

  1. 把 info() 拆成两次单独调用,看代码长多少行。
  2. 给 set_res 加一个「返回实际生效值」的设计,看调用方代码怎么变。
  3. 把重试逻辑塞进封装层,再想想「预览界面」和「后台巡检」两个场景能不能共用。

常见误区

  1. 以为封装的目的是让代码变短。 目的是把协议细节和业务逻辑隔开,行数只是副作用。
  2. 把重试和超时策略包进封装层。 这两个要按场景调,包进去之后每改一次都动所有调用方。
  3. set_res 只返回「成功」。 设备会静默夹边界,要返回实际生效的分辨率, 否则调用方一直以为自己设对了。
自测三问(点开对答案) 1. 封装的判据用一句话怎么说? —— **调用方代码里不该出现 XML。** 一旦上层开始拼信封或写 `re.search` 解报文,就说明界线破了。 2. 为什么重试策略不该包进封装层? —— **因为「重试几次」取决于调用场景**:预览要立刻报错、巡检可以重试。 包进去之后每改一次都要动封装层,牵动所有调用方。 3. `set_res` 为什么应该返回「实际生效的分辨率」? —— **因为设备会静默夹边界**:设 3000×2000 返回成功、回读是 1920×1080。 只说「成功」会让调用方误判。

下一节把这八章的东西串成完整案例:发现、取信息、取配置、取流地址。

相关笔记