
简介C# 语言实现的 ONVIF 协议客户端工具完整源代码基于 Visual Studio 2015 构建。项目包含完整解决方案与工程文件可在 VS2015 中直接打开、编译和调试。面向需要对接网络摄像机、NVR 等安防设备的嵌入式或上位机开发者也适合通过 C# 理解 ONVIF 接口交互逻辑后迁移到其他语言的工程师。工程覆盖设备发现、设备鉴权、参数获取与设置、用户信息管理、固件升级、视频流参数配置并通过 Live555 完成 RTSP 流的解析与显示设备发现模块可探测网内 ONVIF 设备鉴权部分展示了常用安全认证流程固件升级模块演示了文件上传与状态反馈过程。压缩包约 31.69MB共 2000 个文件主体为 1813 个 C# 源文件配合 XAML 界面、DLL 库及 C/C 头文件等构成完整可读工程目录结构便于按模块检索和复用。资源已有 1409 人浏览学习适合协议分析、二次开发或跨语言移植参考。1. 这份 ONVIF C# 工具源码VS2015 打开就能接摄像头的底稿我接触 ONVIF C# 工具是从一台查不到型号的半球机开始的。甲方说摄像头能出画面但平台就是拉不了流我抱着笔记本蹲在机柜边上最后是靠 ONVIF 的 GetStreamUri 接口把 RTSP 地址挖出来才解决的。ONVIF 是安防设备事实上的通用协议不管海康、天地伟业还是杂牌代工只要支持 ONVIF就能用同一套 C# 代码完成设备发现、读取型号、取流、控制云台。这份源码是 VS2015 下的完整工程协议层代码直接可读不需要额外授权改改 IP 和账号就能连设备。适合安防集成商、做视频平台的开发、以及每天要接不同品牌设备的运维。源码的价值不在界面在于把 ONVIF 那套 WS-Discovery、WS-Security、MediaService 调用变成了你能改的 C# 代码。2. 从 WSDL 到可编译工程服务引用、代理类与最小调用2.1 源码包结构拿到手先分清四块代码这份源码在 VS2015 里打开 .sln 就能编工程引用了 .NET Framework 4.5/4.6 自带的 System.ServiceModel不需要额外装 WCF 包。我拿到任何 ONVIF 源码的第一件事是把代码按职责分成四块界面层是 WinForm 的列表和按钮协议层是设备服务、媒体服务、PTZ 服务的调用封装鉴权层是 WS-Security 头的构造工具层是 WS-Discovery 的 UDP 收发。改得最多的永远是协议层和鉴权层界面只是把结果摆出来。先看一眼工程里这几个关键文件DeviceClient 相关文件对应http://www.onvif.org/ver10/device/wsdl负责 GetDeviceInformation、GetCapabilities、GetSystemDateAndTimeMediaClient 对应http://www.onvif.org/ver10/media/wsdl负责 GetProfiles、GetStreamUriPtzClient 对应http://www.onvif.org/ver20/ptz/wsdl负责 ContinuousMove、Stop。如果你拿到手的代码里这些代理类是空的或者编译报错多半是服务引用没生成成功往下看 2.2。2.2 服务引用这关WSDL 和 XSD 要本地化ONVIF 的 WSDL 有一个特别烦人的特点主 WSDL 文件靠import引用一堆外部 XSD比如 common.xsd、types.xsd。VS2015 的 Add Service Reference 在拉取这些远程引用时经常因为网络超时、证书不可信、或者 xsd 路径带版本参数而失败。失败现象很统一生成了一堆空的 partial class编译时找不到DevicePortType这种接口。我一般不让 VS 直接连设备拉 WSDL而是先把文件下载到本地。用浏览器打开http://设备IP/onvif/device_service?wsdl把主 WSDL 保存下来再把里面 import 的每个 XSD 逐个下载到同一个目录最后用文本编辑器把 import 的 location 改成相对路径。这步做完再在 VS 里添加服务引用时直接选本地文件一次性通过。如果嫌手工下载慢用 svcutil 也行但参数要到位不然输出的一堆文件里会有大量冗余配置svcutil http://192.168.1.64/onvif/device_service?wsdl ^ /namespace:*,ONVIF.DeviceProxy ^ /out:DeviceProxy.cs ^ /noConfig/noConfig是关键ONVIF 的绑定不能指望它生成的配置文件后面要自己控制绑定。svcutil 最大的坑是它会尝试拉取所有外部 XSD网络一抖动就中断所以最稳的方式还是 2.1 里说的把 WSDL 和 XSD 全下载到本地再跑。2.3 绕开 Service Reference手工拼 SOAP 的最小调用说实话ONVIF 的 WSDL 生成代理类代码量很大出问题排查也费劲。我更常干的是手工拼 SOAP 消息用 HttpWebRequest 直接 POST。ONVIF 是 SOAP 1.2 协议请求体是 XML整段报文自己能控制鉴权头往哪放也清楚。static string CallOnvif(string url, string bodyXml) { string envelope ?xml version\1.0\ encoding\utf-8\? s:Envelope xmlns:s\http://www.w3.org/2003/05/soap-envelope\ s:Body bodyXml /s:Body/s:Envelope; HttpWebRequest req (HttpWebRequest)WebRequest.Create(url); req.Method POST; req.ContentType application/soapxml; charsetutf-8; req.Timeout 5000; byte[] postBytes Encoding.UTF8.GetBytes(envelope); using (Stream stream req.GetRequestStream()) { stream.Write(postBytes, 0, postBytes.Length); } try { using (HttpWebResponse resp (HttpWebResponse)req.GetResponse()) using (StreamReader reader new StreamReader(resp.GetResponseStream(), Encoding.UTF8)) { return reader.ReadToEnd(); } } catch (WebException ex) { HttpWebResponse errorResp (HttpWebResponse)ex.Response; if (errorResp ! null errorResp.StatusCode HttpStatusCode.Unauthorized) { return HTTP 401: 鉴权失败先检查账号权限或本地时间; } throw; } }这里有两个细节要注意。一是SOAPAction头在 SOAP 1.2 里不是必需的很多设备按 SOAP 1.1 兼容处理才认识它所以我这个最小函数里没放遇到老设备不响应时再补上不迟。二是异常处理里必须拿ex.Response看状态码只打ex.Message根本看不出是 401 还是 404。很多人在这一步翻车把远程服务器返回错误 (401)当成网络不通其实问题出在鉴权。3. 设备发现在 3702 端口用 UDP 组播把摄像头捞出来3.1 发现原理Probe 消息该发到什么地址ONVIF 的设备发现基于 WS-Discovery 协议定义在http://schemas.xmlsoap.org/ws/2005/04/discovery。客户端向组播地址239.255.255.250:3702发一条 Probe 消息设备收到后判断自己是不是符合符合就单播一条 ProbeMatch 回来。这里注意是组播不是广播广播在某些交换机上会被限制而组播是 ONVIF 设备默认监听的。Probe 消息本质上是 SOAP over UDP。你可以把设备发现想象成在局域网里喊一嗓子「谁是 ONVIF 设备报上名来」设备回应的时候会带上自己的 XAddrs服务地址列表、Types设备类型和 Scopes作用域。XAddrs 是最关键的后面所有 ONVIF 接口调用都要用它拼 URL。有个经验很多国产设备对带d:Typesdn:NetworkVideoTransmitter/d:Types的 Probe 反而爱答不理用不带 Types 的通配 Probe 却秒回。我怀疑是某些固件对 Types 的枚举值判断和标准不完全一致所以第一版实现我建议直接发不带 Types 的 Probe拿到设备列表再慢慢过滤。3.2 收发 ProbeMatch完整 C# 代码与参数说明下面这段是我实际在用的发现逻辑UDP 绑定 3702 端口同时负责发送和接收。注意ReceiveTimeout必须设否则udp.Receive会永远阻塞设备不存在时整个线程卡死。public static Liststring DiscoverOnvifDevices(int timeoutMs 5000) { Liststring xaddrList new Liststring(); using (UdpClient udp new UdpClient()) { udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); udp.Client.Bind(new IPEndPoint(IPAddress.Any, 3702)); udp.JoinMulticastGroup(IPAddress.Parse(239.255.255.250)); udp.MulticastLoopback true; udp.Client.ReceiveTimeout timeoutMs; string probe BuildProbeXml(); byte[] buffer Encoding.UTF8.GetBytes(probe); udp.Send(buffer, buffer.Length, new IPEndPoint(IPAddress.Parse(239.255.255.250), 3702)); while (true) { try { IPEndPoint remote new IPEndPoint(IPAddress.Any, 0); byte[] recv udp.Receive(ref remote); string xmlString Encoding.UTF8.GetString(recv); if (xmlString.Contains(ProbeMatch)) { string xaddr ParseXAddrs(xmlString); if (!string.IsNullOrEmpty(xaddr)) xaddrList.Add(xaddr); } } catch (SocketException ex) { if (ex.SocketErrorCode SocketError.TimedOut) break; throw; } } } return xaddrList; } static string BuildProbeXml() { string uuid uuid: Guid.NewGuid().ToString(); return ?xml version\1.0\ encoding\UTF-8\? e:Envelope xmlns:e\http://www.w3.org/2003/05/soap-envelope\ xmlns:w\http://schemas.xmlsoap.org/ws/2004/08/addressing\ xmlns:d\http://schemas.xmlsoap.org/ws/2005/04/discovery\ e:Header w:MessageID uuid /w:MessageID w:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/w:To w:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/w:Action /e:Header e:Bodyd:Probe //e:Body/e:Envelope; }参数说明ReuseAddress允许重复绑定 3702 端口避免上次程序没退干净导致 bind 失败JoinMulticastGroup把本机加入组播组Windows 上不加入是收不到组播包的ReceiveTimeout决定了最多等多久我一般给 5 秒设备多或跨交换机时加到 8 秒。ParseXAddrs用XDocument解析XNamespace前缀对应http://schemas.xmlsoap.org/ws/2005/04/discovery取第一个wsd:XAddrs节点的文本值即可。如果你收到的 ProbeMatch 里 XAddrs 是空说明设备没配好或者固件有 bug这种设备即使发现了也连不上。3.3 跨三层发现Remote Discovery 的边界组播发现只能覆盖同一个二层广播域跨 VLAN 或跨路由是发现不到的。ONVIF 规范里有个 Remote Discovery 机制客户端可以往指定的管理地址发单播 Probe但这种功能大多是平台级设备才支持普通摄像头基本没有。所以你在写工具时要设计成「本地发现 手动输入 IP」两条路手动输入 IP 时直接拼http://ip/onvif/device_service去试很多设备就算 WS-Discovery 是坏的这个入口也是活的。4. 鉴权到取流UsernameToken 摘要、RTSP 地址和 PTZ 的完整链路4.1 PasswordDigest 计算nonce、created 与密码的拼接顺序ONVIF 接口默认要鉴权鉴权方式不是简单的用户名密码而是 WS-Security 的 UsernameToken密码部分用 PasswordDigest 摘要。算法拆开是四步生成随机 nonce、取当前 UTC 时间、把 nonce 字节 created 字符串字节 密码字节拼一起、做 SHA1 摘要后再 Base64。这一步拼错顺序必 401没有任何商量余地。static string BuildPasswordDigest(byte[] nonceBytes, DateTime createdUtc, string password) { string created createdUtc.ToString(yyyy-MM-ddTHH:mm:ssZ); byte[] passwordBytes Encoding.UTF8.GetBytes(password); byte[] createdBytes Encoding.UTF8.GetBytes(created); byte[] buf new byte[nonceBytes.Length createdBytes.Length passwordBytes.Length]; Buffer.BlockCopy(nonceBytes, 0, buf, 0, nonceBytes.Length); Buffer.BlockCopy(createdBytes, 0, buf, nonceBytes.Length, createdBytes.Length); Buffer.BlockCopy(passwordBytes, 0, buf, nonceBytes.Length createdBytes.Length, passwordBytes.Length); using (SHA1 sha SHA1.Create()) { return Convert.ToBase64String(sha.ComputeHash(buf)); } }nonce 生成我习惯用RNGCryptoServiceProvider生成 16 字节随机数每次请求都换新的不能复用复用同一个 nonce 在某些设备上会直接拒绝。created 用 UTC 时间格式严格到秒不带毫秒设备端校验时如果发现系统时间差超过 30 秒密码摘要直接无效。所以做 ONVIF 对接的第一件事不是写代码而是确认电脑和摄像头时间一致。组装 Security 头时把下面这段 XML 塞进s:Envelope的s:Header节点里注意命名空间前缀wsse和wsu不能省wsse:Security xmlns:wssehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd wsse:UsernameToken wsse:Usernameadmin/wsse:Username wsse:Password Typehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigestBase64摘要/wsse:Password wsse:Nonce EncodingTypehttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64BinaryBase64Nonce/wsse:Nonce wsu:Created xmlns:wsuhttp://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd2025-01-15T08:30:00Z/wsu:Created /wsse:UsernameToken /wsse:Security把这段加到第 2 章的CallOnvif里位置是 Header 而不是 Body很多第一次写的人把它塞进 Body设备端解析 WS-Security 头失败返回的报错还不直观只给你一个Sender错误码。遇到这种情况先检查 Security 头是不是在s:Header里。4.2 设备信息与能力GetDeviceInformation、GetCapabilities鉴权通了之后第一件事是调 GetDeviceInformation 拿设备基本信息。这个接口的作用是确认你连的设备真的是 ONVIF 设备并且能看到厂家、型号、固件版本。SOAP Body 非常简单tds:GetDeviceInformation xmlns:tdshttp://www.onvif.org/ver10/device/wsdl /返回的 XML 里tds:Manufacturer、tds:Model、tds:FirmwareVersion就是你要的东西。解析用 XDocument 按命名空间取节点就行注意 ONVIF 的命名空间前缀在返回报文里可能叫tds也可能叫别的字别按前缀解析按命名空间 URI 取。GetCapabilities 比设备信息更重要它告诉你这台设备支持哪些能力以及每个子服务的入口地址。用下面的 Body 调用tds:GetCapabilities xmlns:tdshttp://www.onvif.org/ver10/device/wsdl tds:CategoryAll/tds:Category /tds:GetCapabilities返回的tds:Capabilities里有tt:Media、tt:PTZ、tt:Events等子节点每个节点带XAddr属性这是 Media 服务和 PTZ 服务的真实入口 URL。很多摄像头设备服务和媒体服务的地址不同MediaService 的地址一般是http://ip/onvif/media_service但不绝对必须以 GetCapabilities 返回的 XAddr 为准。如果你忽略这一步直接写死 URL连出海康以外的设备大概率要翻车。4.3 RTSP 取流GetProfiles 到 GetStreamUri 的标准三步取流是视频平台对接里最核心的动作。ONVIF 规定了一条路径GetProfiles 拿 Profile 列表GetStreamUri 拿具体某个 Profile 的 RTSP 地址。先调 GetProfilestrt:GetProfiles xmlns:trthttp://www.onvif.org/ver10/media/wsdl /返回的每个trt:Profiles节点上有token属性值是 ProfileToken还有一个tt:Name描述这个 Profile 是主码流还是子码流。你需要记录 token后面 GetStreamUri 的入参就是它。这里有个细节同一台设备可能有多个 Profile比如一个 1080P 主码流、一个 D1 子码流想要哪个流就用哪个 token选错 Profile 拿到的 RTSP 地址分辨率不对。再调 GetStreamUriBody 里带 ProfileTokentrt:GetStreamUri xmlns:trthttp://www.onvif.org/ver10/media/wsdl trt:StreamSetup tt:Stream xmlns:tthttp://www.onvif.org/ver10/schemaRTP-Unicast/tt:Stream tt:Transport xmlns:tthttp://www.onvif.org/ver10/schema tt:ProtocolRTSP/tt:Protocol /tt:Transport /trt:StreamSetup trt:ProfileTokenProfileToken_001/trt:ProfileToken /trt:GetStreamUri返回的tt:Uri节点里就是 RTSP 地址。这个地址通常长这样rtsp://192.168.1.64:554/stream1。注意很多设备的 RTSP 鉴权和 ONVIF 鉴权是分开的ONVIF 调通了不代表 RTSP 能直接连需要把用户名密码拼进地址里rtsp://admin:密码192.168.1.64:554/stream1。拼的时候密码里有特殊字符记得做 URL 编码不然密码里的或#会把地址截断。PTZ 控制走的是另一套接口PtzService。核心是 ContinuousMove 加 StopContinuousMove 启动云台转动参数控制方向Stop 停止。关键参数是 Velocity 向量x 表示水平速度y 表示垂直速度取值范围 -1.0 到 1.01.0 是最大速度负值是反方向ptz:ContinuousMove xmlns:ptzhttp://www.onvif.org/ver20/ptz/wsdl ptz:ProfileTokenProfileToken_001/ptz:ProfileToken ptz:Velocity tt:PanTilt xmlns:tthttp://www.onvif.org/ver10/schema x-0.5 y0 / tt:Zoom xmlns:tthttp://www.onvif.org/ver10/schema x0 / /ptz:Velocity ptz:TimeoutPT5S/ptz:Timeout /ptz:ContinuousMoveTimeout 的PT5S是 ISO8601 时长格式意思是这个转动指令 5 秒后自动停止。如果设成PT0S设备会一直转到收到 Stop 为止。我建议实际项目里 Timeout 给一个有限值防止客户端异常退出云台转不停。Stop 接口要传两个布尔参数分别控制停止水平转动和垂直转动一般两个都给 true。5. ONVIF 接入避坑手册五个最常见翻车现场与解法5.1 设备列表空白多网卡、防火墙与不带 Types 的 Probe现象程序跑起来3702 端口也监听了设备一台都发现不到。原因分三种Windows 防火墙默认拦 UDP 组播入站多网卡机器上组播走了错误的网卡Probe 消息带了dn:NetworkVideoTransmitter类型导致部分设备不应答。解决先关防火墙测一轮确定不是系统层面问题。再检查网卡笔记本上 WiFi 和有线同时开时组播可能只从其中一块网卡出去。多网卡环境要遍历所有 Up 状态网卡对每块网卡单独发一次 Probeforeach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) { if (ni.OperationalStatus ! OperationalStatus.Up) continue; if (ni.NetworkInterfaceType NetworkInterfaceType.Loopback) continue; IPInterfaceProperties props ni.GetIPProperties(); foreach (UnicastIPAddressInformation addr in props.UnicastAddresses) { if (addr.Address.AddressFamily ! AddressFamily.InterNetwork) continue; // 以 addr.Address 为本地地址创建 UdpClient再发 Probe } }最后 Probe 里把d:Types去掉用通配发现。每次排查都按这个顺序来能省掉大量抓包时间。5.2 401 鉴权失败本地时间与设备时间偏差现象Security 头拼了密码也对设备返回 HTTP 401。原因最常见的是电脑本地时间和摄像头系统时间差超过 30 秒PasswordDigest 里带的是本地生成的时间戳设备端校验时间窗口不通过就拒了。第二种是账号没开 ONVIF 权限很多摄像头在 Web 管理页里 RTSP 用户和 ONVIF 用户是分开的。第三种是 nonce 复用了连续请求用了同一个随机数。解决先用GetSystemDateAndTime接口读取设备时间拿它和本地时间对比如果服务端时间能调就校时不能调就改为用设备时间生成 created 字段。账号问题去设备 Web 后台把 ONVIF 用户单独配置一下。nonce 每次请求必须重新生成这是硬性要求偷懒做成固定值必炸。5.3 服务引用生成失败外部 XSD 下载不全现象VS2015 添加服务引用时提示「无法下载 http://.../common.xsd」或者生成出来的代理类找不到类型。原因ONVIF 的 WSDL import 的外部 XSD 路径是相对路径VS 在远程拉取时偶发超时另外部分设备启用了 HTTPS证书是自签的VS 直接拒了。解决不要用 VS 远程拉取。把 WSDL 和所有引用的 XSD 下载到本地修改 WSDL 里的 import location 为相对路径再从本地文件添加服务引用。如果设备只提供 HTTPS 入口先用浏览器把https://ip/onvif/device_service?wsdl的内容另存下来再把里面引用的 XSD 也逐个保存证书问题通过浏览器访问时手动信任就能绕过。5.4 RTSP 地址能取到但 VLC 拉流失败鉴权参数过期与地址拼接现象GetStreamUri 正常返回了一个 RTSP 地址但 VLC 打开报 401 或直接黑屏。原因ONVIF 取流的 RTSP 地址有一部分设备是动态生成的地址里带了过期时间参数过几分钟就失效另一部分设备返回的地址不带用户名密码需要手动拼还有的情况是 ProfileToken 选错了拿到的是不支持 RTSP 的 Profile。解决取流前重新调 GetStreamUri不要缓存地址拿到地址后先检查有没有用户名没有就用rtsp://user:passip:port/...格式补上ProfileToken 如果拿不准逐个尝试直到 VLC 能出画面。补充一点取流成功后建议顺手验证一下码率信息有些设备主码流和子码流的编码方式在 Profile 里能看到H.265 的流在老平台里拉不动。5.5 HTTPS 设备连接失败自签证书的信任问题现象设备开了 HTTPSONVIF 地址是https://ip:8443/onvif/device_service代码一调就抛AuthenticationException或TrustFailure。原因摄像头用的 HTTPS 证书绝大多数是设备自签的.NET 的 ServicePointManager 默认不信任自签证书直接打断 TLS 握手。解决在建立连接前挂上证书校验回调只对目标 IP 放过不要全局无条件信任ServicePointManager.ServerCertificateValidationCallback (sender, certificate, chain, sslPolicyErrors) { if (sslPolicyErrors SslPolicyErrors.None) return true; // 仅在实际项目明确知道目标 IP 是摄像头时返回 true return true; };这个写法有安全争议但在内网设备调试场景下是常见做法。生产环境建议改成校验证书指纹至少校验一下 IP 是否在允许清单里。6. 用这套工具给一台陌生摄像头做 ONVIF 体检拿到一台不确定支持不支持 ONVIF 的摄像头别急着写代码先把体检流程跑一遍。我用了这套源码之后总结出一个固定套路五步下来设备能不能接入、该用哪个 Profile、坑在哪儿全部清楚了。步骤操作预期结果1设备发现能发现或手动输入 IP 能访问 device_service2GetSystemDateAndTime返回设备时间确认与本地偏差小于 30 秒3GetDeviceInformation GetCapabilities返回厂家、型号、Media XAddr4GetProfiles GetStreamUri返回 RTSP 地址VLC 能播放5ContinuousMove Stop云台能转动并停止每一步我在工具里都留了单独的测试按钮体检的时候就按顺序点下来。哪个环节报错直接跳到对应接口的日志不用从头猜。有一次我接一批工控机上用的工业相机发现第 4 步 GetProfiles 返回空列表但设备信息正常排查半天发现是那款相机需要先调SetProfile初始化才能用协议栈和普通摄像头不一样。从那以后我每次接陌生设备都强制走一遍这五步不猜、不跳、不省连厂家技术支持都省了。这套 C# 源码虽然老但 ONVIF 协议十年没大变VS2015 里改改照样能用。希望帮到你。本文还有配套的精品资源点击获取