ONVIF摄像头视频流获取详解:步骤指南与Python例程实践

1.基本流程

  1. 加入组播udp接口,查询子网内在线的ONVIF摄像头的设备地址:
    设备地址形如:http://192.168.0.6/onvif/device_service
    这一步,参看上一篇发文:[ONVIF系列 – 01] 简介 – 设备发现 – 相关工具-CSDN博客
  2. 查询mediaService Uri地址
    mediaService地址形如:
    http://192.168.0.6/onvif/Media
  3. 查询用户的Profiles,得到一个我们需要的Profile
    Profile形如:Profile_1
  4. 发出查询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&amp;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&amp;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&amp;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&amp;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&amp;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)

作者:子正

物联沃分享整理
物联沃-IOTWORD物联网 » ONVIF摄像头视频流获取详解:步骤指南与Python例程实践

发表回复