资讯动态

Python ONVIF协议实战:自动发现设备与RTSP视频流抓取

发布时间:2026/10/15 1:38:47 来源:尧图企业网站定制
简介这份资源面向从事网络视频监控开发、安防集成或Python音视频方向的工程师与学习者聚焦如何通过ONVIF协议发现并连接网络摄像机进而获取Rtsp视频流并完成抓图截图。包内共3个文件包含1个py脚本、1个txt说明和1个msi第三方调用软件压缩包约20.35MB脚本可直接参考ONVIF客户端初始化、媒体服务与Profile获取、Rtsp地址解析等关键环节说明文档则补充了opencv调用与截图思路msi便于快速搭建运行环境。资源围绕设备认证、媒体服务交互、实时流播放与帧抓取等典型场景展开能帮助读者理解跨品牌IP摄像机的标准化通信流程并掌握从连接设备到保存图片的完整链路。目前已有2151人学习下载适合需要快速上手ONVIF与Rtsp联调、排查连接与流处理问题的开发者参考。1. 从一份 Python ONVIF 抓流脚本说起它到底能解决什么手里有一份python 通过onvif协议获取 Rtsp 视频.zip光看名字容易低估它——很多人第一反应是「不就是拿个 RTSP 地址吗」。但真正在项目里接过网络摄像机的人都清楚RTSP 地址从来不是问题问题是你怎么在不知道 IP、不知道通道号、不知道码流类型的前提下把地址自动挖出来。这份资源干的就是这件事用 Python 走 ONVIF 协议去发现设备、拉取能力集、拼出可用的 RTSP 主/子码流地址顺带把抓图截图这条链路也串起来。它适合三类人一是做视频监控集成、需要在多品牌摄像机之间做统一接入的开发者二是做算法落地、要批量拉流喂给检测模型的工程师三是做设备巡检、想脚本化验证一批摄像机在线状态和码流可用性的运维。核心价值在于「自动发现 自动拼地址」省掉手动翻每台设备 Web 后台的重复劳动。下面按「协议原理 → 环境搭建 → 代码落地 → 抓图 → 避坑 → 进阶」的顺序拆开讲。2. ONVIF 发现与 RTSP 地址拼装协议层到底发生了什么2.1 为什么不能直接写死 RTSP 地址写死地址在单机调试时没问题一旦设备换 IP、换通道、换码流类型脚本就全废。ONVIF 的意义是把「设备是谁、有哪些能力、流地址长什么样」变成可查询的接口。它基于 SOAP over HTTP设备端暴露一组 Web Service客户端发 XML 请求设备回 XML 响应。整个链路分两步先做设备发现WS-Discovery再做能力查询和媒体配置查询。WS-Discovery 走的是 UDP 组播端口 3702客户端往239.255.255.250:3702发 Probe 消息网段内支持 ONVIF 的设备会单播回 ProbeMatch里面带设备的服务地址Device Service URL。拿到这个 URL 之后后续所有请求都走 HTTP SOAP不再用组播。这一步是很多人翻车的地方——组播在跨网段、容器网络、部分交换机上会被拦导致「设备明明在线却发现不了」。2.2 从设备服务到媒体服务的三级跳拿到 Device Service URL 后标准流程是三级跳调GetCapabilities从返回的 XML 里找到 Media Service 的地址不同厂商字段位置略有差异有的在Media节点有的还分Media2。调GetProfiles拿到设备支持的媒体配置文件列表每个 Profile 有独立的 token代表一种编码组合分辨率、编码格式、帧率。对每个 Profile 调GetStreamUri请求里指定StreamSetup的Transport协议RTSP和Stream类型RTP-Unicast设备返回该 Profile 对应的完整 RTSP 地址。这三步里GetProfiles返回的 Profile 数量直接决定你能拿到几条流。常见设备会返回主码流高分辨率和子码流低分辨率两个 Profile但有的厂商把第三码流也塞进来token 命名还各不相同不能靠名字猜必须按返回顺序或分辨率字段判断。2.3 认证方式与鉴权头ONVIF 的认证是 HTTP Digest不是 Basic。请求头里要带UsernameToken包含用户名、密码、Nonce、Created 时间戳密码用 SHA1 摘要。自己手写这套摘要很容易出错尤其是时间戳格式和 Nonce 的 Base64 编码。常见做法是用现成的 ONVIF 客户端库把鉴权细节交给库处理自己只关心业务调用。提示如果设备返回 401 且反复重试先确认设备端是否开启了 ONVIF 功能。部分摄像机默认关闭 ONVIF需要在 Web 后台手动打开并单独设置 ONVIF 用户这个用户和 Web 登录用户可能不是同一套。3. 环境搭建与依赖选型把脚本跑起来的第一步3.1 Python 版本与核心依赖这份资源是 Python 实现建议用 Python 3.8 以上。核心依赖通常两类一是 ONVIF 客户端库二是 RTSP 拉流/解码库。ONVIF 这块常见选择是onvif-zeep它基于 zeep 做 SOAP 解析接口封装比较完整RTSP 拉流用opencv-python最省事底层走 FFmpeg抓图直接read()就行。安装命令如下# 建议在虚拟环境里装避免和系统包冲突 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install onvif-zeep opencv-python requestsonvif-zeep会连带装 zeep、requests 等依赖。如果网络环境导致 pip 慢可以换国内镜像源但不要用来源不明的第三方包。装完后先验证导入# 验证依赖是否可用 from onvif import ONVIFCamera import cv2 print(onvif-zeep ok, opencv:, cv2.__version__)3.2 设备发现组播探测的代码实现如果资源里带了发现模块核心逻辑就是发 WS-Discovery Probe。下面是一段常见的发现实现用 socket 直接发 UDP 组播import socket import uuid # WS-Discovery 组播地址和端口ONVIF 标准固定值 MULTICAST_ADDR 239.255.255.250 PORT 3702 def discover_devices(timeout3): # 构造 Probe 消息MessageID 必须是唯一 UUID message_id uuid: str(uuid.uuid4()) probe f?xml version1.0 encodingUTF-8? e:Envelope xmlns:ehttp://www.w3.org/2003/05/soap-envelope xmlns:whttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdl e:Header w:MessageID{message_id}/w:MessageID w:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/w:To w:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/w:Action /e:Header e:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /e:Body /e:Envelope sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) sock.settimeout(timeout) sock.sendto(probe.encode(), (MULTICAST_ADDR, PORT)) devices [] while True: try: data, addr sock.recvfrom(65535) devices.append((addr[0], data.decode(errorsignore))) except socket.timeout: break sock.close() return devices逻辑说明Types字段限定只发现网络视频发送设备避免把其他 ONVIF 设备也捞进来。IP_MULTICAST_TTL设为 2 表示允许跨一个路由跳如果设备在同一网段设为 1 就够。recvfrom循环配合 timeout 收集所有响应超时即认为发现结束。参数上timeout设 3 秒是经验值网络慢可以加到 5 秒但别设太大否则脚本卡住。3.3 用 ONVIFCamera 拉取 RTSP 地址发现到设备 IP 后用ONVIFCamera建立连接并拉流地址from onvif import ONVIFCamera def get_rtsp_urls(ip, port, user, pwd): # 端口默认 80部分设备 ONVIF 走 8000 或 8080 cam ONVIFCamera(ip, port, user, pwd) media cam.create_media_service() profiles media.GetProfiles() results [] for p in profiles: # 指定 RTP-Unicast RTSP拿到的就是标准 rtsp:// 地址 stream_setup { Stream: RTP-Unicast, Transport: {Protocol: RTSP} } uri media.GetStreamUri({ StreamSetup: stream_setup, ProfileToken: p.token }) results.append({ token: p.token, name: getattr(p, Name, ), resolution: f{p.VideoEncoderConfiguration.Resolution.Width}x f{p.VideoEncoderConfiguration.Resolution.Height}, uri: uri.Uri }) return results逻辑说明create_media_service()内部会自动完成 GetCapabilities 那一步把 Media Service 地址解析出来所以不用手动调。GetStreamUri的StreamSetup必须显式指定协议不指定有的设备返回空。返回的uri.Uri里通常已经带上了用户名密码形如rtsp://user:pwdip:554/...如果没带拉流时要自己拼。参数上port是最容易填错的ONVIF 服务端口和 RTSP 端口是两回事前者常见 80/8000/8080后者常见 554/8554。GetStreamUri返回的地址里会带正确的 RTSP 端口不用自己猜。4. 抓图截图与码流处理从 RTSP 到一帧图像4.1 OpenCV 拉流抓图的完整流程拿到 RTSP 地址后抓图就是标准操作import cv2 def snapshot(rtsp_url, save_path, timeout10): cap cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) # 设置超时避免设备无响应时永久阻塞 cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, timeout * 1000) cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, timeout * 1000) if not cap.isOpened(): raise RuntimeError(无法打开 RTSP 流检查地址和网络) # 丢弃前几帧等解码器稳定避免拿到花屏帧 for _ in range(5): cap.read() ret, frame cap.read() cap.release() if not ret: raise RuntimeError(读取帧失败) cv2.imwrite(save_path, frame) return frame.shape # 返回 (高, 宽, 通道)逻辑说明cv2.CAP_FFMPEG强制走 FFmpeg 后端比默认后端对 RTSP 支持更稳。OPEN_TIMEOUT_MSEC和READ_TIMEOUT_MSEC是关键参数不设的话设备掉线时read()会一直阻塞。丢弃前 5 帧是血泪经验——RTSP 流刚建立时解码器还没同步前几帧经常是花屏或灰屏直接存会拿到废图。4.2 主码流与子码流的选择策略抓图场景下子码流通常够用且更快。主码流可能是 4K拉一帧要等关键帧I 帧延迟高子码流一般是 D1 或 720PI 帧间隔短抓图响应快。如果资源里同时返回了主/子码流地址建议抓图用子码流录像或算法分析用主码流。判断哪个是子码流看GetProfiles返回的分辨率字段别靠 token 名字猜。有的设备 token 叫Profile_1和Profile_2有的叫mainStream和subStream命名毫无规律。4.3 批量抓图的并发控制批量抓图时不要无脑开线程。每路 RTSP 拉流都占带宽和解码资源几十路并发直接把机器打满。常见做法是用线程池限制并发数比如 4 到 8 路一批from concurrent.futures import ThreadPoolExecutor def batch_snapshot(tasks, max_workers4): # tasks 是 [(rtsp_url, save_path), ...] results {} with ThreadPoolExecutor(max_workersmax_workers) as pool: futures { pool.submit(snapshot, url, path): url for url, path in tasks } for fut in futures: url futures[fut] try: results[url] fut.result() except Exception as e: results[url] f失败: {e} return results逻辑说明max_workers设 4 是保守值机器性能好可以到 8但超过 16 基本会因解码排队导致超时。每个任务独立打开和释放VideoCapture不要复用同一个 cap 对象跨线程OpenCV 的 cap 不是线程安全的。5. 避坑与排查那些让脚本跑不通的细节5.1 发现不到设备现象脚本发完 Probe 后一个响应都没有但用厂商工具能搜到设备。原因组播被网络环境拦截或者脚本所在主机有多张网卡组播从错误的网卡出去了。解决先确认脚本主机和设备在同一网段多网卡时用IP_MULTICAST_IF指定出口网卡容器环境要加--network host或显式放行 UDP 3702。如果实在发现不了退而求其次手动填设备 IP 直接走 ONVIFCamera 连接跳过发现步骤。5.2 401 鉴权失败现象ONVIFCamera初始化或调GetProfiles时抛 401。原因用户名密码错误、设备未开 ONVIF、或设备要求 Digest 但库发的是 Basic。解决确认设备 Web 后台里 ONVIF 功能已启用且用的是 ONVIF 专用用户onvif-zeep默认支持 Digest如果还失败检查设备固件是否要求 WS-Security 的特定时间戳格式部分老设备对时间偏差敏感把主机时间同步一下。5.3 RTSP 地址拿到但拉流失败现象GetStreamUri返回了地址但cv2.VideoCapture打不开。原因地址里的用户名密码没 URL 编码密码含、#等特殊字符时地址被截断或者 RTSP 端口被防火墙拦。解决对用户名密码做urllib.parse.quote编码再拼进地址用ffplay或vlc先验证地址本身可用排除是 OpenCV 的问题。如果地址里没带认证信息手动拼rtsp://user:pwdip:port/path。5.4 抓图拿到花屏或黑屏现象imwrite存下来的图是绿的、花的或全黑。原因解码器未同步就取帧或取到的是非 I 帧且解码不完整。解决丢弃前若干帧再取或者循环read()直到帧的均值不为 0简单判黑。更稳的做法是连续读 10 帧取最后一帧牺牲一点时间换稳定性。5.5 长时间运行后卡死现象脚本跑几小时后read()不再返回也不报错。原因RTSP 连接被设备断开但 OpenCV 没感知到read()阻塞。解决设置READ_TIMEOUT_MSEC加心跳检测定期检查cap.isOpened()或者干脆定时重建连接比如每 30 分钟重新VideoCapture一次。这个没有银弹重建连接是最省心的后悔药。6. 进阶把 ONVIF 发现封装成可复用工具的几个技巧走到这一步脚本能跑通了但离「能反复用」还差一层封装。我一般会把发现、连接、拉地址、抓图拆成独立函数用一个配置字典驱动这样换设备只改配置不改代码。一个实用的技巧是缓存 Profile 与分辨率的映射关系。设备重启后 Profile token 通常不变但分辨率可能因配置调整而变所以缓存要带过期时间。下面是一个轻量封装import time import urllib.parse class OnvifHelper: def __init__(self, ip, port, user, pwd, cache_ttl300): self.ip ip self.port port self.user user self.pwd pwd self.cache_ttl cache_ttl self._cache None self._cache_time 0 def _encode_cred(self): # 密码含特殊字符时必须编码否则 RTSP 地址会被截断 u urllib.parse.quote(self.user, safe) p urllib.parse.quote(self.pwd, safe) return u, p def get_streams(self, force_refreshFalse): now time.time() if (not force_refresh and self._cache and now - self._cache_time self.cache_ttl): return self._cache from onvif import ONVIFCamera cam ONVIFCamera(self.ip, self.port, self.user, self.pwd) media cam.create_media_service() profiles media.GetProfiles() u, p self._encode_cred() streams [] for prof in profiles: uri media.GetStreamUri({ StreamSetup: { Stream: RTP-Unicast, Transport: {Protocol: RTSP} }, ProfileToken: prof.token }).Uri # 如果设备返回的地址没带认证手动注入 if not in uri.split(//)[1]: uri uri.replace(rtsp://, frtsp://{u}:{p}) streams.append({ token: prof.token, resolution: ( prof.VideoEncoderConfiguration.Resolution.Width, prof.VideoEncoderConfiguration.Resolution.Height ), uri: uri }) self._cache streams self._cache_time now return streams def pick_substream(self): # 按分辨率升序取最小的那路通常就是子码流 streams self.get_streams() return min(streams, keylambda s: s[resolution][0] * s[resolution][1])逻辑说明cache_ttl控制缓存有效期默认 5 分钟避免每次抓图都重新走一遍 ONVIF 查询。pick_substream用分辨率乘积排序取最小比猜 token 名字靠谱得多。_encode_cred处理密码特殊字符这是实际项目里最容易被忽略的一环——测试时用简单密码没事上线换成复杂密码就翻车。验证封装是否可靠我习惯做两件事一是拿同一台设备连续调 100 次get_streams看缓存命中后耗时是否稳定在毫秒级二是模拟设备断网看ONVIFCamera初始化是否会卡住必要时给它套一层超时装饰器。ONVIF 库本身对连接超时控制较弱生产环境里最好用signal或线程池包一层硬超时。从那以后我每次接新设备都强制先跑一遍发现 拉地址 抓图三步验证确认三样都通了再写业务逻辑。这套流程帮我省掉了大量「以为是代码问题、其实是设备配置问题」的排查时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑