实战:用Python+ONVIF库5分钟实现网络摄像头设备发现与PTZ控制
最近在折腾家里的智能安防系统,想把几个不同品牌的网络摄像头统一管理起来,实现自动巡检和云台控制。本以为要写一堆复杂的网络协议代码,结果发现ONVIF这个标准协议配合Python的onvif-zeep库,竟然能在短短几分钟内搞定设备发现、服务获取甚至PTZ控制。今天我就把整个实战过程拆解给你看,无论你是物联网开发者还是自动化脚本爱好者,这套方案都能让你快速对接市面上大多数支持Profile S协议的摄像头。
1. 环境准备与核心库选择
在开始编写代码之前,我们需要先搭建一个合适的开发环境。ONVIF协议基于SOAP和WSDL,这意味着我们需要处理XML和Web服务。Python社区有几个ONVif客户端库,经过我的实际测试,onvif-zeep是目前最稳定、文档最全的选择。
1.1 安装必要的Python包
打开终端,创建一个新的虚拟环境是个好习惯:
python -m venv onvif_env
source onvif_env/bin/activate # Linux/macOS
# 或者 onvif_env\Scripts\activate # Windows
然后安装核心库:
pip install onvif-zeep
注意:
onvif-zeep会自动安装其依赖,包括zeep(SOAP客户端)、httpx等。确保你的Python版本在3.7以上。
除了核心库,我还推荐安装几个辅助工具,方便调试和验证:
pip install python-dotenv # 管理配置文件
pip install rich # 让终端输出更美观
pip install ipython # 交互式调试
1.2 理解ONVIF Profile S的关键作用
在动手写代码前,花两分钟理解一下ONVIF的Profile概念至关重要。Profile可以理解为“功能套餐”,它定义了一组设备必须支持的最小功能集合。
| Profile 名称 | 主要应用领域 | 核心功能 |
|---|---|---|
| Profile S | 视频流系统 | 视频流获取、PTZ控制、事件处理 |
| Profile T | 高级视频流 | 在Profile S基础上增加智能分析、元数据流 |
| Profile G | 边缘存储 | 本地录像存储与检索 |
| Profile Q | 快速安装 | 简化设备发现与初始配置 |
我们今天的重点Profile S,是网络摄像头最基础、支持最广泛的Profile。一个符合Profile S标准的摄像头,必须提供:
- 设备发现(WS-Discovery)
- 设备管理服务(获取设备信息、网络配置)
- 媒体服务(获取视频流URI、快照)
- PTZ服务(云台控制、预置位)
这意味着,只要你的摄像头支持Profile S(市面上90%以上的主流品牌都支持),我们接下来的代码就能直接控制它的云台转动、变焦和对焦。
2. 快速实现网络摄像头设备发现
设备发现是ONVIF交互的第一步。传统方式需要手动输入摄像头的IP地址,但ONVIF提供了WS-Discovery协议,可以让设备自动在网络上“广播”自己的存在。
2.1 使用WS-Discovery探测局域网设备
创建一个名为discover_cameras.py的文件:
#!/usr/bin/env python3
"""
ONVIF设备发现脚本
使用WS-Discovery协议探测局域网内所有支持ONVIF的设备
"""
import socket
import struct
from datetime import datetime
from rich.console import Console
from rich.table import Table
# 创建控制台输出对象
console = Console()
def discover_onvif_devices(timeout=3):
"""
使用WS-Discovery协议发现ONVIF设备
参数:
timeout: 探测超时时间(秒)
返回:
list: 发现的设备信息列表
"""
# WS-Discovery多播地址和端口
MULTICAST_GROUP = "239.255.255.250"
MULTICAST_PORT = 3702
# 创建UDP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 设置超时
sock.settimeout(timeout)
# 绑定到任意地址和端口
sock.bind(('', MULTICAST_PORT))
# 加入多播组
mreq = struct.pack("4sl", socket.inet_aton(MULTICAST_GROUP), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
# WS-Discovery探测消息
probe_message = '''<?xml version="1.0" encoding="UTF-8"?>
<e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope"
xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"
xmlns:dn="http://www.onvif.org/ver10/network/wsdl">
<e:Header>
<w:MessageID>uuid:{message_id}</w:MessageID>
<w:To e:mustUnderstand="true">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>
<w:Action e:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>
</e:Header>
<e:Body>
<d:Probe>
<d:Types>dn:NetworkVideoTransmitter</d:Types>
</d:Probe>
</e:Body>
</e:Envelope>'''
# 生成唯一的Message ID
import uuid
message_id = str(uuid.uuid4())
probe_msg = probe_message.format(message_id=message_id)
# 发送探测消息
sock.sendto(probe_msg.encode(), (MULTICAST_GROUP, MULTICAST_PORT))
devices = []
console.print(f"[bold green]开始探测局域网ONVIF设备,超时时间{timeout}秒...[/bold green]")
try:
while True:
data, addr = sock.recvfrom(4096)
response = data.decode('utf-8', errors='ignore')
# 解析响应,提取设备信息
device_info = parse_discovery_response(response, addr[0])
if device_info:
devices.append(device_info)
console.print(f"[green]发现设备: {device_info['manufacturer']} - {device_info['model']} ({addr[0]})[/green]")
except socket.timeout:
console.print("[yellow]设备探测完成[/yellow]")
finally:
sock.close()
return devices
def parse_discovery_response(xml_response, ip_address):
"""解析WS-Discovery响应XML"""
import re
device_info = {
'ip': ip_address,
'manufacturer': '未知',
'model': '未知',
'serial_number': '未知',
'hardware_id': '未知'
}
# 使用正则表达式提取关键信息(简化版,实际项目建议用XML解析器)
manufacturer_match = re.search(r'<tt:Manufacturer>(.*?)</tt:Manufacturer>', xml_response)
model_match = re.search(r'<tt:Model>(.*?)</tt:Model>', xml_response)
serial_match = re.search(r'<tt:SerialNumber>(.*?)</tt:SerialNumber>', xml_response)
hardware_match = re.search(r'<tt:HardwareId>(.*?)</tt:HardwareId>', xml_response)
if manufacturer_match:
device_info['manufacturer'] = manufacturer_match.group(1)
if model_match:
device_info['model'] = model_match.group(1)
if serial_match:
device_info['serial_number'] = serial_match.group(1)
if hardware_match:
device_info['hardware_id'] = hardware_match.group(1)
# 提取服务端点(XAddrs)
xaddrs_match = re.search(r'<d:XAddrs>(.*?)</d:XAddrs>', xml_response)
if xaddrs_match:
device_info['service_url'] = xaddrs_match.group(1).split()[0] if xaddrs_match.group(1) else None
return device_info
def display_devices_table(devices):
"""以表格形式显示发现的设备"""
if not devices:
console.print("[red]未发现任何ONVIF设备[/red]")
return
table = Table(title="发现的ONVIF设备", show_header=True, header_style="bold magenta")
table.add_column("序号", style="dim", width=6)
table.add_column("IP地址", style="cyan", width=15)
table.add_column("制造商", width=20)
table.add_column("型号", width=20)
table.add_column("序列号", width=25)
for i, device in enumerate(devices, 1):
table.add_row(
str(i),
device['ip'],
device['manufacturer'],
device['model'],
device['serial_number']
)
console.print(table)
console.print(f"[bold]共发现 {len(devices)} 个设备[/bold]")
if __name__ == "__main__":
# 执行设备发现
discovered_devices = discover_onvif_devices(timeout=5)
# 显示结果
display_devices_table(discovered_devices)
# 保存设备信息到文件
if discovered_devices:
import json
with open('discovered_devices.json', 'w') as f:
json.dump(discovered_devices, f, indent=2, ensure_ascii=False)
console.print(f"[green]设备信息已保存到 discovered_devices.json[/green]")
运行这个脚本,你会在终端看到一个漂亮的表格,列出所有发现的ONVIF设备:
python discover_cameras.py
2.2 处理常见的设备发现问题
在实际使用中,你可能会遇到设备无法被发现的情况。以下是几个常见问题及解决方案:
问题1:防火墙阻止了多播流量
- 解决方案:临时关闭防火墙测试,或添加规则允许UDP端口3702的多播流量
问题2:设备不支持WS-Discovery
- 解决方案:使用手动配置模式,直接指定设备的服务URL
问题3:网络交换机配置限制
- 解决方案:确保交换机启用了IGMP Snooping,或尝试在同一个VLAN中测试
提示:如果自动发现失败,可以尝试使用
nmap扫描摄像头的常用端口(80、443、554、2020),找到设备后再手动配置。
3. 建立ONVIF连接与获取服务能力
成功发现设备后,下一步是建立正式的ONVIF连接。这是最关键的一步,因为我们需要获取设备的服务端点(Endpoints)和能力(Capabilities)。
3.1 创建ONVIF客户端连接
创建一个新的文件camera_client.py:
#!/usr/bin/env python3
"""
ONVIF客户端连接与能力获取
"""
from onvif import ONVIFCamera
import ssl
from urllib.parse import urlparse
from rich.console import Console
from rich.panel import Panel
console = Console()
class ONVIFCameraClient:
def __init__(self, ip, port=80, username='admin', password=''):
"""
初始化ONVIF相机客户端
参数:
ip: 相机IP地址
port: HTTP端口(通常是80或443)
username: 用户名
password: 密码
"""
self.ip = ip
self.port = port
self.username = username
self.password = password
# 构建基础URL
self.base_url = f"http://{ip}:{port}" if port != 443 else f"https://{ip}"
console.print(f"[bold]正在连接相机 {ip}:{port}...[/bold]")
# 创建ONVIF相机对象
try:
# 注意:有些相机需要指定wsdl路径,这里使用库自带的
self.camera = ONVIFCamera(
ip, port, username, password,
wsdl_dir='/usr/local/share/wsdl' # 可能需要调整路径
)
console.print("[green]✓ ONVIF客户端创建成功[/green]")
except Exception as e:
console.print(f"[red]✗ 创建客户端失败: {e}[/red]")
raise
def get_device_information(self):
"""获取设备基本信息"""
try:
info = self.camera.devicemgmt.GetDeviceInformation()
device_info = {
'制造商': info.Manufacturer,
'型号': info.Model,


217

被折叠的 条评论
为什么被折叠?



