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

抓拍不是「从视频流里截一帧」,而是「HTTP GET 一张 JPEG」。 这个区别带来的好处很实际:不用建流、不用等关键帧、不用解码。

📝 本节和前面几节的关系

上一节讲「取流」,这一节讲「不取流也能拿画面」。 在很多业务里(电子地图、报警联动、首页缩略图)根本不需要流, 只要一张图——这时候用抓拍比取流便宜一个数量级。

一、两条路:抓拍与截帧

做法 怎么拿图 代价
ONVIF 抓拍 GetSnapshotUri + HTTP GET 一次请求,几十毫秒
自己截帧 取流 → 解码 → 存一帧 建流 + 等关键帧 + 解码

第一行是「设备帮你做完了」: 设备内部解码一帧、编成 JPEG、用 HTTP 吐出来——客户端只负责 GET。 第二行是「你自己做一遍」: 那就要建 RTSP 连接、等 I 帧、解码、再编码——同样的结果,成本差一个数量级。 所以「要图」和「要流」是两个不同的需求, 别用取流的方式去满足「只要一张图」的需求—— 这是很多客户端卡顿的根源:它为了显示缩略图,给每台设备建了一条流。

二、地址是设备自己的 HTTP 服务

GetSnapshotUri 返回的地址有三个特点:

特点 例子 要注意
不是 SOAP 端点 /onvif/snapshot/… 不能再发 SOAP
可能需要鉴权 HTTP Basic 或 Digest GET 时要带凭证
路径各家不同 /snapshot 或 /jpg 必须用它给的

第一行是最常见的误解: 有人拿到这个地址之后,又往里发了一条 SOAP 请求—— 设备当然不认,回 400 或者一个 HTML 错误页。 第三行说明「不能拼路径」:/onvif/snapshot/profile1 只是这台设备的写法, 换个厂家可能是 /cgi-bin/snapshot.cgi——所以必须用 GetSnapshotUri 拿,不能自己拼。 第二行在调试时最容易忘:浏览器直接打开这个地址会弹登录框, 说明它用的是 HTTP Basic——而代码里 urlopen 不带凭证,就会收到 401。

三、JPEG 的两个字节

抓回来的数据开头一定是 FF D8,结尾是 FF D9:

标记 字节 含义
SOI FF D8 图像开始
EOI FF D9 图像结束

这两个字节是「判断拿到的是不是图」的最快办法: 拿到 401 的 HTML 时,开头是 <!DOCTYPE 或 <html——两字节一比就知道。 这和 08.01.01 那个「WSDL 看前 5 个字节」是同一个思路:「下载成功」不等于「内容对」, 而判断内容对不对,看魔数最快。 顺带一个数: 本机 320×240 的一帧编成 JPEG 质量 85 是 14415 字节——按这个比例估,1080p 大概是几百 KB。 所以「抓拍图片回传」在带宽上是有代价的,尤其是一次抓几十台的时候。

四、高并发抓拍会压住设备

这一节最后一个工程经验:

现象 原因 对策
图越来越慢 设备编码器被占满 限并发,比如 2~3 路
偶尔返回旧图 设备缓存了上一张 加时间戳参数破缓存
偶尔 500 设备内部超时 重试 + 退避

第一行是必然的:抓拍要占用编码器(要编一张 JPEG), 而编码器同时还要出视频流——一台低端 IPC 的编码器可能只够编一路主码流, 你去抓十张图,它就得停下来给你编十次。 第二行那个「旧图」很坑:有的设备的抓拍接口是「返回最近一次缓存的图」, 所以你在 URL 后面加个时间戳参数,有的设备会重新抓、有的不会——要看厂家实现。 第三行那个「重试 + 退避」是通用做法:抓拍失败时立刻重试往往还是失败(设备正忙), 等一两秒再试成功率高得多。

五、动手 Demo:抓拍就是一次 HTTP GET

Demo 分两边:设备端把一帧编成 JPEG 用 HTTP 吐出去, 客户端 urlopen 拿回来——这就是抓拍的全部。 顺便验证「拿回来的字节和编码出来的完全一致」。

# 设备端:解码一帧 → 编 JPEG → 用 HTTP 吐
arr = next(av.open("_t35_enc.mp4").decode(video=0)).to_ndarray(format="bgr24")
ok, buf = cv2.imencode(".jpg", arr, [cv2.IMWRITE_JPEG_QUALITY, 85])
jpg = buf.tobytes()

# 客户端:一次 GET
data = urllib.request.urlopen(real_url, timeout=3).read()
print(len(data), data[:2].hex(), data == jpg)

实跑输出:

--------------------------------------------------------------------
抓拍就是一次 HTTP GET
--------------------------------------------------------------------
**很多人以为「抓拍」是从视频流里截一帧**,
**其实 ONVIF 的做法是给一个独立的 JPEG 地址**:
**客户端直接 HTTP GET 下来,跟 RTSP 完全无关。**

**好处很实际**:
**不用建流、不用等关键帧、不用解码**——
**一次 GET 就是一张图,几十毫秒的事。**

`GetSnapshotUri` 返回:`http://127.0.0.1:52719/onvif/snapshot/profile1`

**这个地址是设备自己的 HTTP 服务**,不是 ONVIF 的 SOAP 端点。
**所以要把它当真 URL 去 GET,而不是再发一个 SOAP 请求。**

**设备端的活儿就是这三步**:解码一帧、编成 JPEG、用 HTTP 吐出去。
**本机照做一遍**(320 × 240 的一帧,JPEG 质量 85):

编码结果 14415 字节,头部两个字节 `ffd8`

**客户端那边就一行 `urlopen`**:

GET `http://127.0.0.1:52721/onvif/snapshot/profile1`
拿回 14415 字节,46.1 毫秒,和编码出来的完全一致:**一致**

**注意返回体的前两个字节是 `ffd8`**,这是 JPEG 的标志(SOI)。
**抓拍失败的常见原因就三个**:
**一是这个地址要鉴权,GET 时不带令牌被拒**;
**二是路径写错(各家不一样,必须用 `GetSnapshotUri` 拿)**;
**三是设备同时只能响应有限个抓拍请求**——
**高并发抓拍会把设备压住,表现为「图越来越慢」。**

最后那个 data[:2].hex() 打出 ffd8, 这就是 JPEG 的 SOI 标记——判断「拿到的是不是图」只看这两个字节。 data == jpg 那个 True 说明「传输没有改动内容」,这一点在调试时有用: 如果拿到的大小和预期差很多,说明中间被拦了(比如代理插了个错误页)。 输出里那 23.8 毫秒是本机回环, 真实设备上要算上「设备编码一帧的时间」—— 那个才是大头(低端设备编一张 1080p JPEG 要几百毫秒)。 所以「抓拍快不快」主要取决于设备,不取决于网络——这也是为什么「并发抓几十台」会把设备压住。

30 秒实验

  1. 把 JPEG 质量从 85 改成 50,看字节数掉多少。
  2. 在 Handler 里加一句 print(self.headers),看客户端到底发了什么请求头。
  3. 把 data[:2] 换成一个 401 的响应体,看前两个字节是什么。

常见误区

  1. 往 GetSnapshotUri 返回的地址发 SOAP 请求。 那是设备的 HTTP 服务,不是 SOAP 端点, 再发 SOAP 只会得到 400。
  2. 自己拼抓拍路径。 各家不一样(/snapshot、/cgi-bin/snapshot.cgi), 必须用 GetSnapshotUri 拿。
  3. 为了显示缩略图给每台设备建一条流。 抓拍一次只要几十毫秒,建流要等关键帧, 成本差一个数量级。
自测三问(点开对答案) 1. 抓拍和「从流里截一帧」的差别在哪? —— **抓拍是设备内部做好、用 HTTP 给一张 JPEG**;截帧要你自己建流、等关键帧、解码。 **成本差一个数量级。** 2. 怎么判断抓回来的是图不是错误页? —— **看前两个字节是不是 `ffd8`**(JPEG 的 SOI)。错误页开头是 `

下一节改分辨率:一个「总是返回成功」的操作,靠什么判断真改没改。

  • ☐ (本节完,下一节 08.07 修改分辨率)

相关笔记