资讯详情 C# ONVIF客户端工具开发实战:VS 2015下设备发现与RTSP取流
📅 2026/10/11 3:51:03
简介这是一套基于 C# 实现的 ONVIF 协议客户端工具源代码面向安防监控、视频平台开发及物联网设备接入方向的开发者适合具备一定 C# 与网络编程基础、希望深入理解 ONVIF 接口调用流程的中高级人员。工具覆盖设备发现、鉴权、参数获取与设置、用户信息管理、固件升级以及视频流参数配置与 RTSP 流获取显示并借助 Live555 完成流媒体解析播放对其他语言分析 ONVIF 各接口也有参考价值。压缩包共约 2000 个文件以 1813 个 cs 源码为核心辅以 1141 个 c、530 个 h、199 个 cpp 等底层实现文件另有 xaml 界面、xml 配置、dll 依赖及 png 资源整体约 31.69MB。内容预览可见 vc1dec.c、ffmpeg.c、h264.c 等编解码相关源码说明包内还整合了流媒体处理链路。目前已有 1409 人学习下载读者可据此梳理 ONVIF 设备交互全流程、RTSP 播放实现与工程目录组织方式快速搭建可运行的客户端原型。1. ONVIF 客户端工具到底解决什么问题从一台摄像头的发现开始手里有一批网络摄像头来自不同厂商Web 界面各不相同想批量改 IP、拉 RTSP 地址、抓一张实时截图难道要挨个登录后台点一遍ONVIF 就是为这种场景生的。它是一套基于 SOAP over HTTP 的开放接口标准把设备发现、能力查询、媒体配置、PTZ 控制、事件订阅这些操作统一成可编程的 Web Service。用 C# 写一个 ONVIF 客户端工具本质就是拿 HttpClient 拼 SOAP 报文按 WSDL 约定的字段发出去再解析返回的 XML。标题里的「C# 版本 ONVIF 协议客户端工具 源代码 VS 2015」指向很明确一套能在 Visual Studio 2015 里直接打开、编译、运行的 C# 工程实现 ONVIF 客户端的核心交互。VS 2015 意味着 .NET Framework 4.5/4.6 时代的技术栈不能用 .NET Core 那套HttpClientFactory、System.Text.Json得老老实实用HttpWebRequest或早期HttpClientXML 处理靠XmlDocument或XDocument。这套东西适合谁做安防集成的、做视频平台对接的、需要把摄像头能力封装成自己业务接口的开发者。新手能照着把设备发现和取流地址跑通熟手能看清 SOAP 报文的构造边界和鉴权坑。2. 用 C# 在 VS 2015 里搭出 ONVIF 客户端骨架2.1 先想清楚为什么用 SOAP 手写而不是直接引 WSDLONVIF 官方提供了一堆 WSDL理论上可以用svcutil.exe生成代理类。但实际做下来我一般不建议新手一上来就生成代理。原因有三个第一ONVIF 的 WSDL 互相引用生成出来的类又大又乱VS 2015 加载慢调试时断点跳来跳去第二不同厂商对 WSDL 的实现有偏差生成的强类型代理遇到字段缺失直接抛异常反而不好排查第三手写 SOAP 报文能让你清楚每个字段从哪来、到哪去出问题时抓包一看就明白。常见做法是用HttpWebRequest发 POSTContent-Type 设application/soapxml; charsetutf-8Body 里拼 Envelope。下面是一个最小可用的 SOAP 发送方法。// SoapClient.cs using System; using System.IO; using System.Net; using System.Text; public class SoapClient { // 发送 SOAP 请求返回响应 XML 字符串 public static string Send(string url, string soapAction, string bodyXml, string user, string pass) { var request (HttpWebRequest)WebRequest.Create(url); request.Method POST; request.ContentType application/soapxml; charsetutf-8; // ONVIF 的 SOAPAction 必须带引号很多设备校验这个头 request.Headers.Add(SOAPAction, \ soapAction \); request.Timeout 8000; // WS-Security UsernameToken 摘要鉴权后面 2.3 会展开 string envelope BuildEnvelope(bodyXml, user, pass); byte[] data Encoding.UTF8.GetBytes(envelope); request.ContentLength data.Length; using (var stream request.GetRequestStream()) { stream.Write(data, 0, data.Length); } try { using (var response (HttpWebResponse)request.GetResponse()) using (var reader new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { return reader.ReadToEnd(); } } catch (WebException ex) { // 设备返回 400/500 时错误详情在响应流里别直接吞掉 if (ex.Response ! null) { using (var errStream ex.Response.GetResponseStream()) using (var errReader new StreamReader(errStream)) { throw new Exception(SOAP Fault: errReader.ReadToEnd()); } } throw; } } }逻辑说明HttpWebRequest是 VS 2015 时代最稳的选择HttpClient在 .NET 4.5 上虽然能用但默认不发送SOAPAction头需要额外配置。参数上soapAction必须和 WSDL 里定义的一模一样比如设备发现是http://www.onvif.org/ver10/network/wsdl/Discovery媒体服务是http://www.onvif.org/ver10/media/wsdl/GetProfiles。Timeout设 8 秒是个经验值太短局域网设备还没响应太长界面卡死。注意WebException里一定要读ex.ResponseONVIF 设备报错时会把SOAP Fault放在响应体里不读就永远不知道是鉴权失败还是参数不对。2.2 设备发现WS-Discovery 的 UDP 组播怎么发ONVIF 设备发现不走 HTTP走的是 WS-Discovery基于 UDP 组播地址239.255.255.250:3702。这是新手第一个容易翻车的点以为发个 HTTP 请求就能找到设备结果什么都收不到。正确做法是构造一个 Probe 报文通过UdpClient发到组播地址然后监听响应。// Discovery.cs using System; using System.Net; using System.Net.Sockets; using System.Text; public class Discovery { public static void Probe() { // WS-Discovery 组播地址和端口固定值 var multicastAddr IPAddress.Parse(239.255.255.250); int port 3702; var udp new UdpClient(); udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); udp.Client.Bind(new IPEndPoint(IPAddress.Any, 0)); // 设置组播生存时间1 表示只在本网段 udp.Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 1); string probe ?xml version1.0 encodingutf-8? s:Envelope xmlns:shttp://www.w3.org/2003/05/soap-envelope xmlns:ahttp://schemas.xmlsoap.org/ws/2004/08/addressing s:Header a:Action s:mustUnderstand1http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/a:Action a:MessageIDuuid:84ede3de-7dec-11d0-c360-f01234567890/a:MessageID a:To s:mustUnderstand1urn:schemas-xmlsoap-org:ws:2005:04:discovery/a:To /s:Header s:Body Probe xmlnshttp://schemas.xmlsoap.org/ws/2005/04/discovery d:Types xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdldn:NetworkVideoTransmitter/d:Types /Probe /s:Body /s:Envelope; byte[] data Encoding.UTF8.GetBytes(probe); udp.Send(data, data.Length, new IPEndPoint(multicastAddr, port)); // 监听响应超时 3 秒 udp.Client.ReceiveTimeout 3000; var remote new IPEndPoint(IPAddress.Any, 0); try { while (true) { byte[] recv udp.Receive(ref remote); string resp Encoding.UTF8.GetString(recv); // 从响应里提取 XAddrs就是设备的服务地址 Console.WriteLine(发现设备: remote.Address - resp); } } catch (SocketException) { // 超时正常结束 } finally { udp.Close(); } } }逻辑说明Probe报文里的MessageID必须是 UUID 格式随便填一个合法的就行但同一网段里别重复。Types填dn:NetworkVideoTransmitter表示只找视频发送设备不找录像机。MulticastTimeToLive设 1 是防止报文跨路由局域网发现够用。参数上ReceiveTimeout设 3000 毫秒因为有些设备响应慢设太短会漏掉。收到响应后关键字段是XAddrs里面是设备的http://ip:port/onvif/device_service地址后续所有 SOAP 请求都往这个地址发。注意UdpClient绑定IPAddress.Any和端口 0让系统自动分配本地端口不要绑 3702否则和系统服务冲突。2.3 WS-Security 鉴权为什么你的请求总是 401ONVIF 设备默认要求鉴权用的是 WS-Security UsernameToken密码不能明文传要用 SHA1 摘要。这是第二个大坑很多人直接把密码填进PasswordText设备返回 401 或者NotAuthorized。正确做法是生成PasswordDigest公式是Base64(SHA1(Nonce Created Password))。// WsSecurity.cs using System; using System.Security.Cryptography; using System.Text; public class WsSecurity { // 生成 WS-Security UsernameToken 头 public static string BuildSecurityHeader(string user, string pass) { // Nonce 是随机数每次请求都要变 byte[] nonceBytes new byte[16]; using (var rng new RNGCryptoServiceProvider()) { rng.GetBytes(nonceBytes); } string nonce Convert.ToBase64String(nonceBytes); // Created 是 UTC 时间格式必须带 Z string created DateTime.UtcNow.ToString(yyyy-MM-ddTHH:mm:ssZ); // PasswordDigest Base64(SHA1(Nonce Created Password)) string raw nonce created pass; byte[] rawBytes Encoding.UTF8.GetBytes(raw); byte[] hash; using (var sha1 SHA1.Create()) { hash sha1.ComputeHash(rawBytes); } string digest Convert.ToBase64String(hash); return string.Format( wsse:Security xmlns:wssehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd xmlns:wsuhttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd wsse:UsernameToken wsse:Username{0}/wsse:Username wsse:Password Typehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest{1}/wsse:Password wsse:Nonce EncodingTypehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary{2}/wsse:Nonce wsu:Created{3}/wsu:Created /wsse:UsernameToken /wsse:Security, user, digest, nonce, created); } }逻辑说明Nonce必须是每次请求新生成的随机数用RNGCryptoServiceProvider而不是Random后者可预测。Created用 UTC 时间格式yyyy-MM-ddTHH:mm:ssZ设备会校验时间偏差如果客户端和设备时间差超过几分钟鉴权会失败这是第三个坑。PasswordDigest的计算顺序不能错Nonce 在前Created 居中Password 在后拼接后做 SHA1再 Base64。参数上nonceBytes长度 16 字节是常见值有些设备要求至少 16 字节。注意wsse:Password的Type属性必须写PasswordDigest写成PasswordText设备会拒绝。3. 把 SOAP 报文拼对设备信息、媒体配置、RTSP 地址获取3.1 GetDeviceInformation 和 GetProfiles 的报文构造骨架搭好后第一步是拿设备信息确认连通性。GetDeviceInformation是最简单的请求Body 里空元素就行。拿到厂商、型号、固件版本后再调GetProfiles拿媒体配置里面包含视频编码、分辨率、帧率以及最关键的StreamUri。// OnvifDevice.cs public class OnvifDevice { private string _serviceUrl; private string _user; private string _pass; public OnvifDevice(string serviceUrl, string user, string pass) { _serviceUrl serviceUrl; _user user; _pass pass; } // 获取设备信息 public string GetDeviceInformation() { string body GetDeviceInformation xmlnshttp://www.onvif.org/ver10/device/wsdl/; string action http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation; return SoapClient.Send(_serviceUrl, action, body, _user, _pass); } // 获取媒体配置 public string GetProfiles() { string body GetProfiles xmlnshttp://www.onvif.org/ver10/media/wsdl/; string action http://www.onvif.org/ver10/media/wsdl/GetProfiles; // 媒体服务的地址通常和 device_service 不同需要先查能力 string mediaUrl _serviceUrl.Replace(/device_service, /media_service); return SoapClient.Send(mediaUrl, action, body, _user, _pass); } // 获取 RTSP 流地址 public string GetStreamUri(string profileToken) { string body string.Format( GetStreamUri xmlnshttp://www.onvif.org/ver10/media/wsdl StreamSetup Stream xmlnshttp://www.onvif.org/ver10/schemaRTP-Unicast/Stream Transport xmlnshttp://www.onvif.org/ver10/schema ProtocolRTSP/Protocol /Transport /StreamSetup ProfileToken{0}/ProfileToken /GetStreamUri, profileToken); string action http://www.onvif.org/ver10/media/wsdl/GetStreamUri; string mediaUrl _serviceUrl.Replace(/device_service, /media_service); return SoapClient.Send(mediaUrl, action, body, _user, _pass); } }逻辑说明GetDeviceInformation的 Body 是空元素但命名空间必须写对http://www.onvif.org/ver10/device/wsdl是设备服务的命名空间。GetProfiles和GetStreamUri属于媒体服务命名空间是http://www.onvif.org/ver10/media/wsdl服务地址通常是把device_service替换成media_service但这不是标准规定稳妥做法是先调GetCapabilities拿到媒体服务的真实地址。参数上GetStreamUri的StreamSetup里Stream填RTP-Unicast表示单播Protocol填RTSP有些设备还支持HTTP但 RTSP 最通用。ProfileToken从GetProfiles的响应里取每个 profile 有一个 token 属性。3.2 解析 XML 响应XPath 取值和命名空间处理拿到响应 XML 后解析是第四个坑。ONVIF 的响应里命名空间很多用XmlDocument.SelectSingleNode时如果不带命名空间管理器XPath 会返回 null。正确做法是创建XmlNamespaceManager把用到的命名空间都注册进去。// XmlParser.cs using System.Xml; public class XmlParser { // 从 GetProfiles 响应里提取所有 ProfileToken public static Liststring ParseProfileTokens(string xml) { var doc new XmlDocument(); doc.LoadXml(xml); var nsmgr new XmlNamespaceManager(doc.NameTable); // 注册媒体服务命名空间前缀随便起但要和 XPath 里一致 nsmgr.AddNamespace(trt, http://www.onvif.org/ver10/media/wsdl); nsmgr.AddNamespace(tt, http://www.onvif.org/ver10/schema); var tokens new Liststring(); // Profiles 元素在 trt 命名空间下token 是属性 var nodes doc.SelectNodes(//trt:Profiles, nsmgr); foreach (XmlNode node in nodes) { var attr node.Attributes[token]; if (attr ! null) { tokens.Add(attr.Value); } } return tokens; } // 从 GetStreamUri 响应里提取 RTSP 地址 public static string ParseStreamUri(string xml) { var doc new XmlDocument(); doc.LoadXml(xml); var nsmgr new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace(trt, http://www.onvif.org/ver10/media/wsdl); nsmgr.AddNamespace(tt, http://www.onvif.org/ver10/schema); // Uri 元素在 tt 命名空间下 var node doc.SelectSingleNode(//trt:GetStreamUriResponse/trt:MediaUri/tt:Uri, nsmgr); return node ! null ? node.InnerText : null; } }逻辑说明XmlNamespaceManager必须注册响应里出现的所有命名空间否则 XPath 匹配不到。前缀trt和tt是自定义的只要和 XPath 里一致就行但命名空间 URI 必须和响应里完全一样。GetStreamUriResponse的结构是MediaUri下面有Uri、InvalidAfterConnect、InvalidAfterReboot、Timeout四个子元素Uri就是 RTSP 地址。参数上SelectNodes返回的是XmlNodeList遍历时注意Attributes[token]可能为 null要做判空。注意有些设备的响应里命名空间前缀和标准不一样但 URI 是一样的所以用 URI 注册命名空间最稳。3.3 用 WinForm 做一个最小的设备管理界面VS 2015 里 WinForm 是最快能出界面的选择。一个TextBox填设备地址一个Button触发发现一个DataGridView展示设备列表和 RTSP 地址。核心逻辑就是把前面的Discovery.Probe、OnvifDevice.GetProfiles、XmlParser.ParseStreamUri串起来。// MainForm.cs private void btnDiscover_Click(object sender, EventArgs e) { // 发现设备是阻塞操作放后台线程避免界面卡死 Task.Run(() { var devices Discovery.ProbeAndReturn(); // 返回设备地址列表 this.Invoke(new Action(() { dgvDevices.Rows.Clear(); foreach (var dev in devices) { // 对每个设备查 RTSP 地址 var onvif new OnvifDevice(dev.ServiceUrl, txtUser.Text, txtPass.Text); string profilesXml onvif.GetProfiles(); var tokens XmlParser.ParseProfileTokens(profilesXml); if (tokens.Count 0) { string streamXml onvif.GetStreamUri(tokens[0]); string rtsp XmlParser.ParseStreamUri(streamXml); dgvDevices.Rows.Add(dev.IpAddress, dev.Model, rtsp); } } })); }); }逻辑说明Task.Run把发现和查询放到线程池避免 UI 线程阻塞。this.Invoke回到 UI 线程更新控件这是 WinForm 跨线程更新的标准做法。参数上txtUser和txtPass从界面读取默认可以填admin但不同厂商默认账号不同常见的是admin加空密码或者admin加admin123。注意Discovery.ProbeAndReturn需要把前面的 UDP 接收逻辑封装成返回列表超时后返回已发现的设备。dgvDevices的列可以设 IP、型号、RTSP 地址三列RTSP 地址可以直接复制到播放器里验证。4. 避坑与排查ONVIF 客户端最常见的 5 个翻车现场4.1 发现不到设备先查防火墙和网卡现象Probe发出去一个响应都收不到。原因通常是 Windows 防火墙拦了 UDP 3702 的入站或者机器有多张网卡组播发到了错误的网卡。解决在防火墙里放行 UDP 3702或者临时关闭防火墙测试。多网卡时用udp.Client.SetSocketOption绑定指定网卡的 IP而不是IPAddress.Any。另外确认摄像头和电脑在同一网段跨网段组播默认不转发。4.2 鉴权失败时间偏差和 Nonce 复用现象设备返回 401 或者NotAuthorized。原因有两个一是客户端和设备时间差超过 5 分钟Created校验不过二是Nonce在多次请求里复用了设备认为重放攻击。解决每次请求都重新生成Nonce并且用DateTime.UtcNow而不是本地时间。如果设备时间不准先通过GetSystemDateAndTime接口拿设备时间再校准本地计算。4.3 媒体服务地址拼错不是所有设备都叫 media_service现象GetProfiles返回 404 或者ActionNotSupported。原因媒体服务的路径不是固定的有些设备是/onvif/media_service有些是/onvif/media还有些是/onvif/services。解决先调GetCapabilities从响应里找Media节点的XAddr那才是真实地址。不要硬编码替换字符串。4.4 RTSP 地址拿到但播不了鉴权和传输协议现象GetStreamUri返回了地址但 VLC 打不开。原因RTSP 地址里不带用户名密码需要手动拼成rtsp://user:passip:port/...或者设备要求用RTP-Unicast但实际只支持RTP-Multicast。解决在 RTSP 地址里插入鉴权信息注意密码里的特殊字符要 URL 编码。如果还不行把StreamSetup里的Protocol从RTSP改成HTTP试试有些设备只支持 HTTP 隧道。4.5 VS 2015 编译报错目标框架和引用缺失现象工程打开后一堆红线提示找不到System.Net.Http或者System.Security。原因VS 2015 默认新建的是 .NET 4.5但有些 API 在 4.5 里没有或者项目引用没加全。解决把目标框架改成 .NET Framework 4.6 或 4.6.1在「引用」里手动添加System.Net.Http、System.Security、System.Xml。如果用了Task.Run确认System.Threading.Tasks已引用。注意 VS 2015 的 NuGet 版本较老装包时如果提示不兼容手动下载对应版本的 nupkg 放到本地源。5. 进阶把 ONVIF 客户端封装成可复用的库5.1 用接口隔离设备差异别让厂商特例污染主流程做到后面你会发现不同厂商对 ONVIF 的实现差异很大有的GetProfiles返回的 token 是数字有的是 UUID有的GetStreamUri必须带StreamSetup有的不带也能返回有的设备发现响应里XAddrs是 HTTPS有的是 HTTP。如果把这些特例都写进主流程代码会变成一堆if-else。我的做法是定义一个IOnvifTransport接口把 SOAP 发送、鉴权头构造、XML 解析抽象出来每个厂商的差异用一个子类覆盖。public interface IOnvifTransport { string Send(string url, string action, string body); string BuildAuthHeader(); } public class StandardOnvifTransport : IOnvifTransport { // 标准实现用 WS-Security UsernameToken } public class LegacyOnvifTransport : IOnvifTransport { // 某些老设备只支持 HTTP Digest覆盖 Send 方法 }逻辑说明接口隔离的好处是主流程只依赖IOnvifTransport新增厂商支持时只加一个实现类不改动OnvifDevice里的业务逻辑。参数上BuildAuthHeader返回完整的Security头 XMLSend负责拼 Envelope 和发请求。注意接口方法不要设计得太细否则每个厂商都要实现一堆空方法两三个方法足够覆盖大部分场景。5.2 用配置文件管理设备列表和轮询间隔实际项目里不会每次手动填设备地址通常是把设备列表放在 XML 或 JSON 配置文件里程序启动时加载定时轮询设备状态。VS 2015 里可以用ConfigurationManager读App.config或者用XmlSerializer读写自定义 XML。!-- devices.xml -- Devices Device Name前门/Name ServiceUrlhttp://192.168.1.64/onvif/device_service/ServiceUrl Useradmin/User Passadmin123/Pass PollInterval30/PollInterval /Device /Devices逻辑说明PollInterval单位是秒表示每隔多久调一次GetDeviceInformation检查设备是否在线。参数上ServiceUrl必须是完整的device_service地址不能只写 IP。User和Pass明文存储有安全风险生产环境应该加密但 VS 2015 时代常用DPAPI的ProtectedData类做本地加密。注意轮询不要并发太高10 个设备以内用单线程顺序查就行多了再考虑Parallel.ForEach并限制并发数。5.3 一个验证技巧用抓包对照 SOAP 报文调 ONVIF 最有效的手段是抓包。用 Wireshark 过滤http或者udp.port 3702看发出去的报文和收到的响应。我一般会先抓一次成功的请求把报文存下来后面出问题就对比字段差异。比如鉴权失败时对比PasswordDigest的计算结果看 Nonce 和 Created 是否一致。这个习惯帮我省了很多猜的时间。另外ONVIF 设备通常有一个GetServiceCapabilities接口返回支持的媒体、PTZ、事件服务列表调一次就能知道设备到底支持哪些功能不用一个个试。希望帮到你。本文还有配套的精品资源点击获取