ONVIF摄像头视频流获取详解:步骤指南与Python例程实践
1.基本流程
- 加入组播udp接口,查询子网内在线的ONVIF摄像头的设备地址:
设备地址形如:http://192.168.0.6/onvif/device_service
这一步,参看上一篇发文:[ONVIF系列 – 01] 简介 – 设备发现 – 相关工具-CSDN博客 - 查询mediaService Uri地址
mediaService地址形如:
http://192.168.0.6/onvif/Media - 查询用户的Profiles,得到一个我们需要的Profile
Profile形如:Profile_1 - 发出查询Profile详情请求给ONVIF mediaServiceUri得到最终的媒体流Uri:
形如:rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1
1.1 Python代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 获取当前脚本文件所在目录的父目录,并构建相对路径
import os
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
project_path = os.path.join(current_dir, '..')
sys.path.append(project_path)
sys.path.append(current_dir)
import asyncio
import aiohttp
import xml.etree.ElementTree as ET
from httpx import AsyncClient, DigestAuth
import httpx
# ONVIF 设备服务 URL
device_service_url = 'http://192.168.0.6/onvif/device_service'
username = 'xxxxx'
password = 'xxxxxxx'
async def get_device_information(session, url):
headers = {'Content-Type': 'application/soap+xml'}
body = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<s:Header/>
<s:Body>
<tds:GetServices>
<tds:IncludeCapability>false</tds:IncludeCapability>
</tds:GetServices>
</s:Body>
</s:Envelope>"""
try:
response = httpx.post(url, headers=headers, data=body, auth=DigestAuth(username, password))
return response.text
except Exception as e:
print(f'An error occurred: {e}')
return None
def parse_media_service_url(device_response):
media_service_url = None
root = ET.fromstring(device_response)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'tds': 'http://www.onvif.org/ver10/device/wsdl'}
services = root.find('.//tds:GetServicesResponse', namespaces=ns)
#print(services)
# 查找 <tds:XAddr> 元素
for s in services:
ns1 = s.find('.//tds:Namespace', namespaces=ns)
if(ns1 is not None):
print('........ns................',ns1.text)
if('media' in ns1.text) and ('ver10' in ns1.text):
addr = s.find('.//tds:XAddr', namespaces = ns)
if(addr is not None):
#print('........addr................',addr)
media_service_url = addr.text
return media_service_url
async def get_media_profiles(session, media_service_url):
headers = {'Content-Type': 'application/soap+xml'}
body = """<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope" xmlns:media="http://www.onvif.org/ver10/media/wsdl">
<soapenv:Header/>
<soapenv:Body>
<media:GetProfiles/>
</soapenv:Body>
</soapenv:Envelope>"""
try:
response = httpx.post(media_service_url, headers=headers, data=body, auth=DigestAuth(username, password))
return response.text
except Exception as e:
print(f'An error occurred: {e}')
return None
def parse_media_profile(profile_response):
profile_token = None
root = ET.fromstring(profile_response)
ns = {'trt': 'http://www.onvif.org/ver10/media/wsdl', 'tt': 'http://www.onvif.org/ver10/schema'}
profiles = root.find('.//trt:GetProfilesResponse', namespaces=ns)
# 查找 <tds:XAddr> 元素
for s in profiles:
profile_token = s.get('token')
print(profile_token)
break
return profile_token
async def get_video_stream_url(session, media_service_url, profileToken):
headers = {'Content-Type': 'application/soap+xml'}
body = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:t="http://www.onvif.org/ver10/media">
<s:Body>
<t:GetStreamUri>
<t:StreamSetup>
<t:Stream>RTP-Unicast</t:Stream>
<t:Transport>
<t:Protocol>RTSP</t:Protocol>
</t:Transport>
</t:StreamSetup>
<t:ProfileToken>YourProfileToken</t:ProfileToken>
</t:GetStreamUri>
</s:Body>
</s:Envelope>"""
try:
src_sub_str = '<t:ProfileToken>YourProfileToken</t:ProfileToken>'
real_sub_str = f'<t:ProfileToken>{profileToken}</t:ProfileToken>'
body = body.replace(src_sub_str, real_sub_str)
response = httpx.post(media_service_url, headers=headers, data=body, auth=DigestAuth(username, password))
return response.text
except Exception as e:
print(f'An error occurred: {e}')
return None
def parse_video_stream_url(media_response):
root = ET.fromstring(media_response)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'trt': 'http://www.onvif.org/ver10/media/wsdl','tt': 'http://www.onvif.org/ver10/schema'}
uri = root.find('.//trt:GetStreamUriResponse//trt:MediaUri//tt:Uri', namespaces=ns) #//trt:MediaUri
if uri is not None:
return uri.text
return None
async def main():
async with httpx.AsyncClient() as session:
device_response = await get_device_information(session, device_service_url)
#print(device_response)
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>step1 get media soap addr')
media_service_url = parse_media_service_url(device_response)
print(media_service_url)
if not media_service_url:
print("Media service URL not found")
return
profile_response = await get_media_profiles(session, media_service_url)
#print(profile_response)
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>step2 get profile token')
profile = parse_media_profile(profile_response)
#print(profile)
if not profile:
print("Media profile not found")
media_response = await get_video_stream_url(session, media_service_url, profile)
print(media_response)
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>step3 get stream url')
video_stream_url = parse_video_stream_url(media_response)
print("Video Stream URL:", video_stream_url)
# 运行异步主函数
if __name__ == '__main__':
asyncio.run(main())
1.2 代码说明
1.2.1 权限控制
soap消息如果涉及权限控制,asyncio需要借助httpx才能进行——就是ONVIF,Post Soap消息时涉及的auth=DigestAuth()。
1.2.2 soap消息
soap消息是一个xml, 我们使用ET.fromstring(device_response)来进行处理。
1.2.3 xml解析的名字空间
xml解析中一个重要的概念是ns,名字空间,如果我们需要的信息被包裹为:
<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1</tt:Uri>
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
<tt:Timeout>PT60S</tt:Timeout>
</trt:MediaUri>
</trt:GetStreamUriResponse>
注意xml元素的被冒号:分割的前导部分,比如trt,tt,这些元素在进行解析前需要预先声明:
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'tds': 'http://www.onvif.org/ver10/device/wsdl'}
1.2.4 xml元素的逐级搜索
xml解析类似json,可以先拿到一个分支节点,再以分支节点为搜索起点,继续搜索,比如:
def parse_media_service_url(device_response):
media_service_url = None
root = ET.fromstring(device_response)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'tds': 'http://www.onvif.org/ver10/device/wsdl'}
services = root.find('.//tds:GetServicesResponse', namespaces=ns)
#print(services)
# 查找 <tds:XAddr> 元素
for s in services:
ns1 = s.find('.//tds:Namespace', namespaces=ns)
if(ns1 is not None):
print('……..ns…………….',ns1.text)
if('media' in ns1.text) and ('ver10' in ns1.text):
addr = s.find('.//tds:XAddr', namespaces = ns)
if(addr is not None):
#print('……..addr…………….',addr)
media_service_url = addr.text
return media_service_url
1.2.5 xml元素的按照路径信息一次定位
def parse_video_stream_url(media_response):
root = ET.fromstring(media_response)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'trt': 'http://www.onvif.org/ver10/media/wsdl','tt': 'http://www.onvif.org/ver10/schema'}
uri = root.find('.//trt:GetStreamUriResponse//trt:MediaUri//tt:Uri', namespaces=ns) #//trt:MediaUri
if uri is not None:
return uri.text
return None
2交互过程的回应帧和特征信息
2.1查询onvif device uri得到media services uri
2.1.1 查询帧
headers = {'Content-Type': 'application/soap+xml'}
body = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<s:Header/>
<s:Body>
<tds:GetServices>
<tds:IncludeCapability>false</tds:IncludeCapability>
</tds:GetServices>
</s:Body>
</s:Envelope>"""
2.1.2 回应帧( 片段)
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
<tds:XAddr>http://192.168.0.6/onvif/Media</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major>
<tt:Minor>60</tt:Minor>
</tds:Version>
</tds:Service>
回应帧中可能有多个media services接口,选择版本最低的就行:
……..ns……………. http://www.onvif.org/ver10/device/wsdl
……..ns……………. http://www.onvif.org/ver10/media/wsdl
……..ns……………. http://www.onvif.org/ver10/events/wsdl
……..ns……………. http://www.onvif.org/ver20/imaging/wsdl
……..ns……………. http://www.onvif.org/ver10/deviceIO/wsdl
……..ns……………. http://www.onvif.org/ver20/analytics/wsdl
……..ns……………. http://www.onvif.org/ver20/media/wsdl
2.2 查询media sevices uri Profiles得到所需Profile
2.2.1 查询帧
headers = {'Content-Type': 'application/soap+xml'}
body = """<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope" xmlns:media="http://www.onvif.org/ver10/media/wsdl">
<soapenv:Header/>
<soapenv:Body>
<media:GetProfiles/>
</soapenv:Body>
</soapenv:Envelope>"""
2.2.2 回应帧(片段)
<trt:GetProfilesResponse><trt:Profiles token="Profile_1" fixed="true"><tt:Name>mainStream</tt:Name>
<tt:VideoSourceConfiguration token="VideoSourceToken"><tt:Name>VideoSourceConfig</tt:Name>
<tt:UseCount>2</tt:UseCount>
<tt:SourceToken>VideoSource_1</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</tt:VideoSourceConfiguration>
<tt:VideoEncoderConfiguration token="VideoEncoderToken_1"><tt:Name>VideoEncoder_1</tt:Name>
<tt:UseCount>1</tt:UseCount>
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution><tt:Width>1920</tt:Width>
<tt:Height>1080</tt:Height>
</tt:Resolution>
<tt:Quality>3.000000</tt:Quality>
<tt:RateControl><tt:FrameRateLimit>10</tt:FrameRateLimit>
<tt:EncodingInterval>1</tt:EncodingInterval>
<tt:BitrateLimit>1024</tt:BitrateLimit>
</tt:RateControl>
<tt:H264><tt:GovLength>20</tt:GovLength>
<tt:H264Profile>Main</tt:H264Profile>
</tt:H264>
2.3 查询 media services uri得到流媒体uri
2.3.1 查询帧
headers = {'Content-Type': 'application/soap+xml'}
body = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:t="http://www.onvif.org/ver10/media">
<s:Body>
<t:GetStreamUri>
<t:StreamSetup>
<t:Stream>RTP-Unicast</t:Stream>
<t:Transport>
<t:Protocol>RTSP</t:Protocol>
</t:Transport>
</t:StreamSetup>
<t:ProfileToken>YourProfileToken</t:ProfileToken>
</t:GetStreamUri>
</s:Body>
</s:Envelope>"""
2.3.2 回应帧(片段)
<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1</tt:Uri>
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
<tt:Timeout>PT60S</tt:Timeout>
</trt:MediaUri>
</trt:GetStreamUriResponse>
附录A 与ONVIF视频流地址获取相关的摄像头回应帧
1. MediaService Uri查询回应
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:soapenc="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tst="http://www.onvif.org/ver10/storage/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl" xmlns:wsoap12="http://schemas.xmlsoap.org/wsdl/soap12" xmlns:http="http://schemas.xmlsoap.org/wsdl/http" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:wsadis="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics" xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl" xmlns:tr2="http://www.onvif.org/ver20/media/wsdl" xmlns:axt="http://www.onvif.org/ver20/analytics"><env:Body><tds:GetServicesResponse><tds:Service><tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>
<tds:XAddr>http://192.168.0.6/onvif/device_service</tds:XAddr>
<tds:Version><tt:Major>17</tt:Major>
<tt:Minor>12</tt:Minor>
</tds:Version>
</tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>
<tds:XAddr>http://192.168.0.6/onvif/Media</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major>
<tt:Minor>60</tt:Minor>
</tds:Version>
</tds:Service>
<tds:Service><tds:Namespace>http://www.onvif.org/ver10/events/wsdl</tds:Namespace>
<tds:XAddr>http://192.168.0.6/onvif/Events</tds:XAddr>
<tds:Version><tt:Major>2</tt:Major>
<tt:Minor>60</tt:Minor>
</tds:Version>
2.Profiles 查询回应
消息中对我们需要的部分做了标注
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:soapenc="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tst="http://www.onvif.org/ver10/storage/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl" xmlns:wsoap12="http://schemas.xmlsoap.org/wsdl/soap12" xmlns:http="http://schemas.xmlsoap.org/wsdl/http" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:wsadis="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics" xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl" xmlns:tr2="http://www.onvif.org/ver20/media/wsdl" xmlns:axt="http://www.onvif.org/ver20/analytics"><env:Body><trt:GetProfilesResponse><trt:Profiles token="Profile_1" fixed="true"><tt:Name>mainStream</tt:Name>
<tt:VideoSourceConfiguration token="VideoSourceToken"><tt:Name>VideoSourceConfig</tt:Name>
<tt:UseCount>2</tt:UseCount>
<tt:SourceToken>VideoSource_1</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</tt:VideoSourceConfiguration>
<tt:VideoEncoderConfiguration token="VideoEncoderToken_1"><tt:Name>VideoEncoder_1</tt:Name>
<tt:UseCount>1</tt:UseCount>
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution><tt:Width>1920</tt:Width>
<tt:Height>1080</tt:Height>
</tt:Resolution>
<tt:Quality>3.000000</tt:Quality>
<tt:RateControl><tt:FrameRateLimit>10</tt:FrameRateLimit>
<tt:EncodingInterval>1</tt:EncodingInterval>
<tt:BitrateLimit>1024</tt:BitrateLimit>
</tt:RateControl>
<tt:H264><tt:GovLength>20</tt:GovLength>
<tt:H264Profile>Main</tt:H264Profile>
</tt:H264>
<tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type>
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
</tt:Address>
<tt:Port>8860</tt:Port>
<tt:TTL>128</tt:TTL>
<tt:AutoStart>false</tt:AutoStart>
</tt:Multicast>
<tt:SessionTimeout>PT5S</tt:SessionTimeout>
</tt:VideoEncoderConfiguration>
<tt:VideoAnalyticsConfiguration token="VideoAnalyticsToken"><tt:Name>VideoAnalyticsName</tt:Name>
<tt:UseCount>2</tt:UseCount>
<tt:AnalyticsEngineConfiguration><tt:AnalyticsModule Name="MyCellMotionModule" Type="tt:CellMotionEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="60"/>
<tt:ElementItem Name="Layout"><tt:CellLayout Columns="22" Rows="18"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.090909" y="0.111111"/>
</tt:Transformation>
</tt:CellLayout>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
<tt:AnalyticsModule Name="MyLineDetectorModule" Type="tt:LineDetectorEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="50"/>
<tt:ElementItem Name="Transformation"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.002000" y="0.002000"/>
</tt:Transformation>
</tt:ElementItem>
<tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="1000"/>
<tt:Point x="1000" y="1000"/>
<tt:Point x="1000" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
<tt:AnalyticsModule Name="MyFieldDetectorModule" Type="tt:FieldDetectorEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="50"/>
<tt:ElementItem Name="Transformation"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.002000" y="0.002000"/>
</tt:Transformation>
</tt:ElementItem>
<tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="1000"/>
<tt:Point x="1000" y="1000"/>
<tt:Point x="1000" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
<tt:AnalyticsModule Name="MyTamperDetecModule" Type="hikxsd:TamperEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="0"/>
<tt:ElementItem Name="Transformation"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.002841" y="0.003472"/>
</tt:Transformation>
</tt:ElementItem>
<tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="576"/>
<tt:Point x="704" y="576"/>
<tt:Point x="704" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
</tt:AnalyticsEngineConfiguration>
<tt:RuleEngineConfiguration><tt:Rule Name="MyMotionDetectorRule" Type="tt:CellMotionDetector"><tt:Parameters><tt:SimpleItem Name="MinCount" Value="5"/>
<tt:SimpleItem Name="AlarmOnDelay" Value="1000"/>
<tt:SimpleItem Name="AlarmOffDelay" Value="1000"/>
<tt:SimpleItem Name="ActiveCells" Value="0P8A8A=="/>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector1" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector2" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector3" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector4" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector1" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="50.000000" y="250.000000"/>
<tt:Point x="50.000000" y="1000.000000"/>
<tt:Point x="950.000000" y="1000.000000"/>
<tt:Point x="950.000000" y="250.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector2" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector3" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector4" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyTamperDetectorRule" Type="hikxsd:TamperDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="0"/>
<tt:Point x="0" y="0"/>
<tt:Point x="0" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
</tt:RuleEngineConfiguration>
</tt:VideoAnalyticsConfiguration>
<tt:Extension></tt:Extension>
</trt:Profiles>
<trt:Profiles token="Profile_2" fixed="true"><tt:Name>subStream</tt:Name>
<tt:VideoSourceConfiguration token="VideoSourceToken"><tt:Name>VideoSourceConfig</tt:Name>
<tt:UseCount>2</tt:UseCount>
<tt:SourceToken>VideoSource_1</tt:SourceToken>
<tt:Bounds x="0" y="0" width="1920" height="1080"></tt:Bounds>
</tt:VideoSourceConfiguration>
<tt:VideoEncoderConfiguration token="VideoEncoderToken_2" encoding="H265"><tt:Name>VideoEncoder_2</tt:Name>
<tt:UseCount>1</tt:UseCount>
<tt:Encoding>H264</tt:Encoding>
<tt:Resolution><tt:Width>640</tt:Width>
<tt:Height>360</tt:Height>
</tt:Resolution>
<tt:Quality>3.000000</tt:Quality>
<tt:RateControl><tt:FrameRateLimit>10</tt:FrameRateLimit>
<tt:EncodingInterval>1</tt:EncodingInterval>
<tt:BitrateLimit>512</tt:BitrateLimit>
</tt:RateControl>
<tt:H264><tt:GovLength>20</tt:GovLength>
<tt:H264Profile>Main</tt:H264Profile>
</tt:H264>
<tt:Multicast><tt:Address><tt:Type>IPv4</tt:Type>
<tt:IPv4Address>0.0.0.0</tt:IPv4Address>
</tt:Address>
<tt:Port>8866</tt:Port>
<tt:TTL>128</tt:TTL>
<tt:AutoStart>false</tt:AutoStart>
</tt:Multicast>
<tt:SessionTimeout>PT5S</tt:SessionTimeout>
</tt:VideoEncoderConfiguration>
<tt:VideoAnalyticsConfiguration token="VideoAnalyticsToken"><tt:Name>VideoAnalyticsName</tt:Name>
<tt:UseCount>2</tt:UseCount>
<tt:AnalyticsEngineConfiguration><tt:AnalyticsModule Name="MyCellMotionModule" Type="tt:CellMotionEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="60"/>
<tt:ElementItem Name="Layout"><tt:CellLayout Columns="22" Rows="18"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.090909" y="0.111111"/>
</tt:Transformation>
</tt:CellLayout>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
<tt:AnalyticsModule Name="MyLineDetectorModule" Type="tt:LineDetectorEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="50"/>
<tt:ElementItem Name="Transformation"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.002000" y="0.002000"/>
</tt:Transformation>
</tt:ElementItem>
<tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="1000"/>
<tt:Point x="1000" y="1000"/>
<tt:Point x="1000" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
<tt:AnalyticsModule Name="MyFieldDetectorModule" Type="tt:FieldDetectorEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="50"/>
<tt:ElementItem Name="Transformation"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.002000" y="0.002000"/>
</tt:Transformation>
</tt:ElementItem>
<tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="1000"/>
<tt:Point x="1000" y="1000"/>
<tt:Point x="1000" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
<tt:AnalyticsModule Name="MyTamperDetecModule" Type="hikxsd:TamperEngine"><tt:Parameters><tt:SimpleItem Name="Sensitivity" Value="0"/>
<tt:ElementItem Name="Transformation"><tt:Transformation><tt:Translate x="-1.000000" y="-1.000000"/>
<tt:Scale x="0.002841" y="0.003472"/>
</tt:Transformation>
</tt:ElementItem>
<tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="576"/>
<tt:Point x="704" y="576"/>
<tt:Point x="704" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:AnalyticsModule>
</tt:AnalyticsEngineConfiguration>
<tt:RuleEngineConfiguration><tt:Rule Name="MyMotionDetectorRule" Type="tt:CellMotionDetector"><tt:Parameters><tt:SimpleItem Name="MinCount" Value="5"/>
<tt:SimpleItem Name="AlarmOnDelay" Value="1000"/>
<tt:SimpleItem Name="AlarmOffDelay" Value="1000"/>
<tt:SimpleItem Name="ActiveCells" Value="0P8A8A=="/>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector1" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector2" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector3" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyLineDetector4" Type="tt:LineDetector"><tt:Parameters><tt:SimpleItem Name="Direction" Value="Any"/>
<tt:ElementItem Name="Segments"><tt:Polyline><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polyline>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector1" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="50.000000" y="250.000000"/>
<tt:Point x="50.000000" y="1000.000000"/>
<tt:Point x="950.000000" y="1000.000000"/>
<tt:Point x="950.000000" y="250.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector2" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector3" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyFieldDetector4" Type="tt:FieldDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:Polygon><tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
<tt:Point x="0.000000" y="0.000000"/>
</tt:Polygon>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
<tt:Rule Name="MyTamperDetectorRule" Type="hikxsd:TamperDetector"><tt:Parameters><tt:ElementItem Name="Field"><tt:PolygonConfiguration><tt:Polygon><tt:Point x="0" y="0"/>
<tt:Point x="0" y="0"/>
<tt:Point x="0" y="0"/>
<tt:Point x="0" y="0"/>
</tt:Polygon>
</tt:PolygonConfiguration>
</tt:ElementItem>
</tt:Parameters>
</tt:Rule>
</tt:RuleEngineConfiguration>
</tt:VideoAnalyticsConfiguration>
<tt:Extension></tt:Extension>
</trt:Profiles>
</trt:GetProfilesResponse>
</env:Body>
</env:Envelope>
3. stream uri – Profile Desc 查询回应
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:soapenc="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tst="http://www.onvif.org/ver10/storage/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl" xmlns:wsoap12="http://schemas.xmlsoap.org/wsdl/soap12" xmlns:http="http://schemas.xmlsoap.org/wsdl/http" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:wsadis="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics" xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl" xmlns:tr2="http://www.onvif.org/ver20/media/wsdl" xmlns:axt="http://www.onvif.org/ver20/analytics"><env:Body><trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1</tt:Uri>
<tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
<tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
<tt:Timeout>PT60S</tt:Timeout>
</trt:MediaUri>
</trt:GetStreamUriResponse>
</env:Body>
</env:Envelope>
附录B 如果需要获取子码流 – python代码
处理只是相较上面的代码对profile做了findall的遍历,实际输出
[None, [{'ip': '192.168.0.6', 'stream_url': 'rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1', 'user': 'admin', 'pass': 'xxxxxx', 'desc': '1920x1080_H264'}, {'ip': '192.168.0.6', 'stream_url': 'rtsp://192.168.0.6:554/Streaming/Channels/102?transportmode=unicast&profile=Profile_2', 'user': 'admin', 'pass': 'xxxxxx', 'desc': '640x360_H264'}]]
B.1 Python源码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 获取当前脚本文件所在目录的父目录,并构建相对路径
import os
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
project_path = os.path.join(current_dir, '..')
sys.path.append(project_path)
sys.path.append(current_dir)
import datetime
import time
import threading
from datetime import datetime
import asyncio
import aiohttp
import xml.etree.ElementTree as ET
from httpx import AsyncClient, DigestAuth
import httpx
import re
import socket
import struct
import asyncio
import aiohttp
'''
usage:
hOnvif = GpONVIF(2, 1975)
arOnvifCameraDesc = hOnvif.GetAllOnlineCameraStreamUrls()
print(arOnvifCameraDesc)
'''
class GpONVIF:
def __init__(self, timeoutInS=2, localPort=1975, onvif_user='admin', onvif_pass='xxxxxx'):
self.cameraLists = []
self.searchIpYet = False
self.timeoutInS = timeoutInS
# 组播地址和端口
self.MULTICAST_GROUP = '239.255.255.250' #fixed, can not be changed.
self.MULTICAST_PORT = 3702 #fixed, can not be changed
self.LOCALPORT_IN_MULTICAST = localPort
self.onvifUser = onvif_user
self.onvifPass = onvif_pass
def fake_result(self):
json = [{"ip":"192.168.0.6",
"stream_url":"rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1",
"user":"admin",
"pass":"xxxxx",
"desc":"1920*1080_H265"}]
return json;
#devices = {'http://192.168.0.2/onvif/device_service':'192.168.0.2', 'http://192.168.0.6/onvif/device_service':'http://192.168.0.6'}
def GetAllOnlineCameras(self):
devices = self.discover_onvif_devices(self.timeoutInS)
print(devices)
return devices;
def GetStreamUrlOfCamera(self, camera_ip, camera_device_service_url, onvif_user, onvif_password):
return asyncio.run(self.LookupCameraStreamUrlFromDeviceServiceInterface(camera_ip, camera_device_service_url, onvif_user, onvif_password))
#不再使用的接口, comment out by fengxh Aug06,2024
def GetAllOnlineCameraStreamUrls(self):
if not self.searchIpYet:
devices = self.discover_onvif_devices(self.timeoutInS)
print(devices)
for deviceServiceUrl in devices:
jsonOfCamera = asyncio.run(self.LookupCameraStreamUrlFromDeviceServiceInterface(devices[deviceServiceUrl], deviceServiceUrl, self.onvifUser, self.onvifPass))
if len(jsonOfCamera)>0:
self.cameraLists.append(jsonOfCamera)
self.searchIpYet = True
return self.cameraLists;
#ipAddrs is Json
def discover_onvif_devices(self, waitTimeInSec):
ipAddrs = {}
# 创建一个 UDP 套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
# 设置多播 TTL
TTL = 2
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, TTL) #TTL 生存时间
# 允许重用地址
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定到本地端口
sock.bind(('', self.LOCALPORT_IN_MULTICAST))
# 禁用组播环回
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 0)
# 加入组播组
mreq = struct.pack("4sl", socket.inet_aton(self.MULTICAST_GROUP), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
is_SendSSDP_request = False
# 构造 SSDP 请求
ssdp_request ='<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"><s:Header><a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action><a:MessageID>uuid:2fe59dad-ae6e-47f2-9b3c-5caba35ad4fc</a:MessageID><a:ReplyTo><a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To></s:Header><s:Body><Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery"><d:Types xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dp0="http://www.onvif.org/ver10/network/wsdl">dp0:NetworkVideoTransmitter</d:Types></Probe></s:Body></s:Envelope>'
# 发送请求到多播地址
sock.sendto(ssdp_request.encode(), (self.MULTICAST_GROUP , self.MULTICAST_PORT))
timeAnchor = datetime.now()
timeLeftInS = waitTimeInSec
while True:
try:
# 设置接收超时时间
sock.settimeout(timeLeftInS)
# 接收响应
response, _ = sock.recvfrom(4096)
print(f'Received response:\n{response.decode()}')
txt_camera_device_service_reply = response.decode()
(deviceIp, serviceUrl) = self.parse_camera_device_service_url_from(txt_camera_device_service_reply)
if deviceIp is not None:
if not (deviceIp in ipAddrs):
if(serviceUrl is not None):
ipAddrs[serviceUrl] = deviceIp
passedTimeYet = (datetime.now() - timeAnchor).total_seconds()
timeLeftInS = waitTimeInSec - passedTimeYet
if(timeLeftInS >0): #Python的integer没有无符号的概念, comment by fengxh.
continue
else:
break
except socket.timeout:
break
# 退出组播组
sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq)
# 关闭套接字
sock.close()
return ipAddrs;
def parse_camera_device_service_url_from(self, xmlCamera_device_service):
'''
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:soapenc="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tev="http://www.onvif.org/ver10/events/wsdl" xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl" xmlns:tan="http://www.onvif.org/ver20/analytics/wsdl" xmlns:tst="http://www.onvif.org/ver10/storage/wsdl" xmlns:ter="http://www.onvif.org/ver10/error" xmlns:dn="http://www.onvif.org/ver10/network/wsdl" xmlns:tns1="http://www.onvif.org/ver10/topics" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl" xmlns:wsoap12="http://schemas.xmlsoap.org/wsdl/soap12" xmlns:http="http://schemas.xmlsoap.org/wsdl/http" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:wsadis="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsnt="http://docs.oasis-open.org/wsn/b-2" xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:wstop="http://docs.oasis-open.org/wsn/t-1" xmlns:wsrf-bf="http://docs.oasis-open.org/wsrf/bf-2" xmlns:wsntw="http://docs.oasis-open.org/wsn/bw-2" xmlns:wsrf-rw="http://docs.oasis-open.org/wsrf/rw-2" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:wsrf-r="http://docs.oasis-open.org/wsrf/r-2" xmlns:trc="http://www.onvif.org/ver10/recording/wsdl" xmlns:tse="http://www.onvif.org/ver10/search/wsdl" xmlns:trp="http://www.onvif.org/ver10/replay/wsdl" xmlns:tnshik="http://www.hikvision.com/2011/event/topics" xmlns:hikwsd="http://www.onvifext.com/onvif/ext/ver10/wsdl" xmlns:hikxsd="http://www.onvifext.com/onvif/ext/ver10/schema" xmlns:tas="http://www.onvif.org/ver10/advancedsecurity/wsdl" xmlns:tr2="http://www.onvif.org/ver20/media/wsdl" xmlns:axt="http://www.onvif.org/ver20/analytics"><env:Header><wsadis:MessageID>urn:uuid:4e774000-6f8b-11b2-8068-240f9bbadc0c</wsadis:MessageID>
<wsadis:RelatesTo>uuid:2fe59dad-ae6e-47f2-9b3c-5caba35ad4fc</wsadis:RelatesTo>
<wsadis:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsadis:To>
<wsadis:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches</wsadis:Action>
<d:AppSequence InstanceId="1722847544" MessageNumber="13"/>
</env:Header>
<env:Body><d:ProbeMatches><d:ProbeMatch><wsadis:EndpointReference><wsadis:Address>urn:uuid:4e774000-6f8b-11b2-8068-240f9bbadc0c</wsadis:Address>
</wsadis:EndpointReference>
<d:Types>dn:NetworkVideoTransmitter tds:Device</d:Types>
<d:Scopes>onvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/Profile/Streaming onvif://www.onvif.org/Profile/T onvif://www.onvif.org/MAC/24:0f:9b:ba:dc:0c onvif://www.onvif.org/hardware/DS-2CD3T25D-I3 onvif://www.onvif.org/name/HIKVISION%20DS-2CD3T25D-I3 onvif://www.onvif.org/location/city/hangzhou</d:Scopes>
<d:XAddrs>http://192.168.0.6/onvif/device_service http://[240e:33d:17:6a0:260f:9bff:feba:dc0c]/onvif/device_service</d:XAddrs>
<d:MetadataVersion>10</d:MetadataVersion>
</d:ProbeMatch>
</d:ProbeMatches>
</env:Body>
</env:Envelope>
'''
root = ET.fromstring(xmlCamera_device_service)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'd': "http://schemas.xmlsoap.org/ws/2005/04/discovery"}
serviceUrls = root.find('.//d:XAddrs', namespaces=ns)
print(serviceUrls.text)
arUrl = serviceUrls.text.split()
# 使用正则表达式提取 IPv4 和 IPv6 地址
ipv4_pattern = r'http://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
for url in arUrl:
ipv4_match = re.search(ipv4_pattern, serviceUrls.text)
if ipv4_match is not None:
# 提取并打印结果
ipv4_address = ipv4_match.group(1) if ipv4_match else None
return (ipv4_address, url)
return (None, None)
async def get_device_information(self, session, url, username, password):
headers = {'Content-Type': 'application/soap+xml'}
body = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
<s:Header/>
<s:Body>
<tds:GetServices>
<tds:IncludeCapability>false</tds:IncludeCapability>
</tds:GetServices>
</s:Body>
</s:Envelope>"""
try:
response = httpx.post(url, headers=headers, data=body, auth=DigestAuth(username, password))
return response.text
except Exception as e:
print(f'An error occurred: {e}')
return None
def parse_media_service_url(self, device_response):
media_service_url = None
root = ET.fromstring(device_response)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'tds': 'http://www.onvif.org/ver10/device/wsdl'}
services = root.find('.//tds:GetServicesResponse', namespaces=ns)
#print(services)
# 查找 <tds:XAddr> 元素
for s in services:
ns1 = s.find('.//tds:Namespace', namespaces=ns)
if(ns1 is not None):
print('........ns................',ns1.text)
if('media' in ns1.text) and ('ver10' in ns1.text):
addr = s.find('.//tds:XAddr', namespaces = ns)
if(addr is not None):
#print('........addr................',addr)
media_service_url = addr.text
return media_service_url
async def get_media_profiles(self, session, media_service_url, username, password):
headers = {'Content-Type': 'application/soap+xml'}
body = """<soapenv:Envelope xmlns:soapenv="http://www.w3.org/2003/05/soap-envelope" xmlns:media="http://www.onvif.org/ver10/media/wsdl">
<soapenv:Header/>
<soapenv:Body>
<media:GetProfiles/>
</soapenv:Body>
</soapenv:Envelope>"""
try:
response = httpx.post(media_service_url, headers=headers, data=body, auth=DigestAuth(username, password))
return response.text
except Exception as e:
print(f'An error occurred: {e}')
return None
def parse_media_profile(self, profile_response):
ret = []
profile_token = None
root = ET.fromstring(profile_response)
ns = {'trt': 'http://www.onvif.org/ver10/media/wsdl', 'tt': 'http://www.onvif.org/ver10/schema'}
profiles = root.findall('.//trt:Profiles', namespaces=ns)
# 查找 <tds:XAddr> 元素
for s in profiles:
profile_token = s.get('token')
try:
encoding = s.find('.//tt:Encoding',namespaces=ns).text
except Exception:
encoding = 'Unknow'
pass
try:
width = int(s.find('.//tt:Resolution//tt:Width',namespaces=ns).text)
height= int(s.find('.//tt:Resolution//tt:Height',namespaces=ns).text)
except Exception:
width=0
height=0
pass
#print(profile_token, encoding, width, height)
ret.append((profile_token, encoding, width, height))
return ret
async def get_video_stream_url(self, session, media_service_url, profileToken, username, password):
headers = {'Content-Type': 'application/soap+xml'}
body = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:t="http://www.onvif.org/ver10/media">
<s:Body>
<t:GetStreamUri>
<t:StreamSetup>
<t:Stream>RTP-Unicast</t:Stream>
<t:Transport>
<t:Protocol>RTSP</t:Protocol>
</t:Transport>
</t:StreamSetup>
<t:ProfileToken>YourProfileToken</t:ProfileToken>
</t:GetStreamUri>
</s:Body>
</s:Envelope>"""
try:
src_sub_str = '<t:ProfileToken>YourProfileToken</t:ProfileToken>'
real_sub_str = f'<t:ProfileToken>{profileToken}</t:ProfileToken>'
body = body.replace(src_sub_str, real_sub_str)
response = httpx.post(media_service_url, headers=headers, data=body, auth=DigestAuth(username, password))
return response.text
except Exception as e:
print(f'An error occurred: {e}')
return None
def parse_video_stream_url(self, media_response):
root = ET.fromstring(media_response)
ns = {'soap': 'http://www.w3.org/2003/05/soap-envelope', 'trt': 'http://www.onvif.org/ver10/media/wsdl','tt': 'http://www.onvif.org/ver10/schema'}
uri = root.find('.//trt:GetStreamUriResponse//trt:MediaUri//tt:Uri', namespaces=ns) #//trt:MediaUri
if uri is not None:
return uri.text
return None
def gen_an_onvif_desc(self):
json = {"ip":"192.168.0.6",
"stream_url":"rtsp://192.168.0.6:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1",
"user":"admin",
"pass":"a1234567",
"desc":"1920*1080_H265"
}
return json;
#return an array
async def LookupCameraStreamUrlFromDeviceServiceInterface(self, ip, device_service_url, username, password):
result_total = []
try:
async with httpx.AsyncClient() as session:
device_response = await self.get_device_information(session, device_service_url, username, password)
#print(device_response)
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>step1 get media soap addr')
media_service_url = self.parse_media_service_url(device_response)
print(media_service_url)
if not media_service_url:
print("Media service URL not found")
return []
profile_response = await self.get_media_profiles(session, media_service_url, username, password)
#print(profile_response)
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>step2 get profile token')
arProfile = self.parse_media_profile(profile_response)
if not arProfile:
print("Media profile not found")
return []
for (profile, encoding, width, height) in arProfile:
print(profile, encoding, width, height)
media_response = await self.get_video_stream_url(session, media_service_url, profile, username, password)
#print(media_response)
print('>>>>>>>>>>>>>>>>>>>>>>>>>>>step3 get stream url')
video_stream_url = self.parse_video_stream_url(media_response)
print("Video Stream URL:", video_stream_url)
result = self.gen_an_onvif_desc()
result["ip"] = ip
result["stream_url"] = video_stream_url
result["user"] = username
result["pass"] = password
result["desc"] = f"{width}x{height}_{encoding}"
result_total.append(result);
except Exception as e:
pass
return result_total
if __name__ == "__main__":
hOnvif = GpONVIF(2, 1975)
cameraDesc = hOnvif.GetAllOnlineCameras()
ret = []
for device_url in cameraDesc:
arOnvifCameraDesc = hOnvif.GetStreamUrlOfCamera(cameraDesc[device_url], device_url, hOnvif.onvifUser, hOnvif.onvifPass)
ret.append(arOnvifCameraDesc)
print(ret)
作者:子正