简介:在Android平台上使用Onvif协议完成局域网摄像头自动发现,并获取可播放的视频流地址,是安防与物联网项目开发中常见的需求。资源面向具备Android基础、需要对接不同品牌Onvif兼容设备的开发者,围绕设备发现、身份认证、媒体服务和视频流解析提供了完整且可直接参考的工程实现。压缩包内共142个文件,整体大小约28.46MB,包括Java源码、类文件、布局与配置、依赖库、动态库以及可直接安装的APK编译产物;代码结构清晰,可从中看到UDP多播搜索、SOAP消息交互、媒体参数获取和RTSP地址提取等关键流程的实现方式。已有2028人学习/下载,既适合快速搭建Android端摄像头发现模块,也适合通过示例理解Onvif协议在移动端的具体落地方法,减少大量自行抓包排错和兼容性验证的时间成本。
1. 厂家SDK五花八门,我为什么坚持先走Onvif
做安防类App的人应该都遇到过这种状况:要接的摄像头来自好几个品牌,海康的SDK是AAR,某家的SDK只有Windows版,还有个牌子连SDK都没给,只留下一份文档让你自己去抓它的RTSP流。最后翻到包装盒背面,三个品牌都印着同一个缩写:Onvif。这就是统一接入的突破口。
当时我的需求很简单:在Android端发现局域网内的所有摄像头,拿到每台设备的播放地址,再交给播放器出画面。听起来像一个几十行代码的小功能,实际拆开之后会发现里面有两条完全不同的技术链路,一条是"设备发现",一条是"取流地址获取",两条链路又分别踩了不少Android特有的坑。这篇就把这两条链路完整展开,给出能直接落地的代码参考和排错思路。
1.1 Onvif协议到底管哪几件事
Onvif全称Open Network Video Interface Forum,它不是一个单一协议,而是一整套面向网络视频设备的互操作规范。跟取流相关的部分可以拆成三条链路:
- 设备发现:基于WS-Discovery,客户端在局域网内发一条UDP组播消息,所有支持Onvif的摄像头都会响应;
- 设备能力与媒体配置:通过SOAP/XML接口查询设备服务地址、媒体服务地址、通道Profile等;
- 媒体流传输:通过GetStreamUri拿到RTSP地址,真正的音视频数据走RTSP/RTP,这层不归Onvif管。
厂家SDK本质上就是把这套流程封装起来,再叠加自己的云服务和私有协议。如果只做局域网内的设备发现和取流,Onvif是覆盖面最广的"最低通用标准"。海康、大华、TP-LINK、宇视这些主流品牌基本都支持,一些小众代工产品只要印了Onvif标志,也大概率能通过同一套逻辑拿到RTSP地址。
1.2 用Onvif的代价和收益
代价很明显:SOAP请求繁琐,不同设备对规范的理解和执行有偏差,网上现成的Android端Onvif库要么年久失修要么文档稀缺。收益也很明确:协议栈写一次,以后新增任何品牌都走同一套逻辑,不需要为某个厂家单独维护SDK版本。
我的建议是,只要产品线里摄像头品牌不是单一固定,Onvif就值得投入。哪怕是主打厂家SDK的项目,也可以把Onvif发现能力当作"兼容模式"兜底。下面这套流程跑通之后,你在Android上就拥有了一个跟品牌无关的摄像头接入层。
2. 设备发现:一条UDP组播消息开启的局域网广播
2.1 WS-Discovery的工作逻辑
Onvif设备发现基于WS-Discovery。设备上电后会加入组播组239.255.255.250:3702,客户端构造一条Probe消息发给这个组播地址,设备收到后单播回复ProbeMatch,里面携带设备类型、作用域Scopes、以及最重要的服务地址XAddrs。
注意"单播回复"这个细节。设备一般是回给你发送方的IP+端口,所以Android端不需要强制绑定3702端口,用一个普通DatagramSocket发出去就能在同一个socket上收回复。有些教程让你用MulticastSocket加joinGroup,那也能工作,但多个工具同时占3702会冲突,不如随机端口干净。
设备收到Probe后通常会等待一小段随机时间再回复,有的200毫秒就回了,有的要等两秒以上。所以发送之后不能立刻放弃,要在合理窗口内持续接收。
2.2 Probe消息模板与收发代码
Probe消息是一段SOAP封装的XML,模板如下:
<?xml version="1.0" encoding="UTF-8"?> <e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope" xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dn="http://www.onvif.org/ver10/network/wsdl"> <e:Header> <w:MessageID>uuid:xxxxxxxx</w:MessageID> <w:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To> <w:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action> </e:Header> <e:Body> <d:Probe> <d:Types>dn:NetworkVideoTransmitter</d:Types> </d:Probe> </e:Body> </e:Envelope>Types里写dn:NetworkVideoTransmitter是只召回网络视频设备,如果要把NVR、门禁这些也一起发现,可以换成设备理解的其他类型,或者干脆留空。MessageID每次请求必须重新生成,否则部分设备直接忽略这条消息。我之前图省事用一个固定UUID,结果某品牌的摄像头一次都不回,排查了半天才定位到是MessageID重复。
发送和接收我放在同一个线程、同一个socket上:
DatagramSocket socket = new DatagramSocket(); socket.setSoTimeout(6000); byte[] payload = probeXml.getBytes(StandardCharsets.UTF_8); socket.send(new DatagramPacket(payload, payload.length, InetAddress.getByName("239.255.255.250"), 3702)); long deadline = System.currentTimeMillis() + 6000; byte[] buffer = new byte[8192]; while (System.currentTimeMillis() < deadline) { try { DatagramPacket resp = new DatagramPacket(buffer, buffer.length); socket.receive(resp); String xml = new String(resp.getData(), 0, resp.getLength(), StandardCharsets.UTF_8); handleProbeMatch(xml); } catch (SocketTimeoutException e) { break; } }解析ProbeMatch时重点抓XAddrs标签,这就是后面所有SOAP调用的入口地址。解析用XmlPullParser按标签本地名匹配,不要去依赖前缀:
XmlPullParser parser = Xml.newPullParser(); parser.setInput(new StringReader(xml)); while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { if (parser.getEventType() == XmlPullParser.START_TAG && "XAddrs".equals(parser.getName())) { String xaddrs = parser.nextText(); // 格式类似 http://192.168.1.64/onvif/device_service } parser.next(); }2.3 MulticastLock这个坑必须提前说
Android在WiFi下默认会过滤组播包,不主动持有MulticastLock,设备即使回复了ProbeMatch,网卡根本不会把包交给应用。这个坑几乎每个第一次写的人都会踩。
WifiManager wifiManager = (WifiManager) getApplicationContext() .getSystemService(Context.WIFI_SERVICE); MulticastLock multicastLock = wifiManager.createMulticastLock("onvif-discovery"); multicastLock.acquire(); // 整个发现流程跑完后再release还有,无论摄像头是POE供电还是独立DC供电,只要手机和它在同一局域网、设备Onvif开关打开,发现逻辑都一样。别在POE摄像头上额外做特殊处理。
3. 拿RTSP地址的SOAP三连:GetCapabilities、GetProfiles、GetStreamUri
3.1 三连是干什么的,能不能省
发现阶段只拿到了设备服务地址,也就是XAddrs里那个/onvif/device_service。这个入口只能查设备级能力,不能直接拿流地址。要得到最终能播的RTSP地址,必须按顺序问三次:
- GetCapabilities:问设备"你的媒体服务在哪",拿到Media XAddr;
- GetProfiles:问媒体服务"你有哪些通道/编码配置",拿到ProfileToken;
- GetStreamUri:问某个Profile"给我流地址",拿到RTSP URI。
能不能省?如果只针对某个品牌,把厂家经验地址直接写死确实能跑,比如海康常见rtsp://IP:554/Streaming/Channels/101,但App就退化成"只支持某几款摄像头"。走完SOAP三连,不同品牌的路径差异会被中间层全部消化掉,后面接新品牌根本不用改代码。
按我个人的习惯,这三步我封装成了一个同步方法getRtspAddress(deviceXAddr, profileToken),业务层只用关心最终返回的字符串。中间任何一步失败,要么返回null要么抛出带步骤信息的异常,方便定位是哪个环节断了。
3.2 每次调用的Action和返回值
三个请求都是标准SOAP POST,Content-Type用application/soap+xml。关键差异我整理成了表:
| 调用 | SOAPAction | Body关键参数 | 返回关键字段 |
|---|---|---|---|
| GetCapabilities | http://www.onvif.org/ver10/device/wsdl/GetCapabilities | Category: All | Capabilities/Media/XAddr |
| GetProfiles | http://www.onvif.org/ver10/media/wsdl/GetProfiles | 空 | Profiles节点上的token属性 |
| GetStreamUri | http://www.onvif.org/ver10/media/wsdl/GetStreamUri | ProfileToken、StreamSetup | MediaUri/Uri |
GetProfiles的请求体大致是这个样子:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:trt="http://www.onvif.org/ver10/media/wsdl"> <soap:Header> <!-- 注意这里要带认证信息,见下文 --> </soap:Header> <soap:Body> <trt:GetProfiles/> </soap:Body> </soap:Envelope>GetStreamUri稍微多两个必填字段:
<trt:GetStreamUri> <trt:StreamSetup> <tt:Stream>RTP-Unicast</tt:Stream> <tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport> </trt:StreamSetup> <trt:ProfileToken>这里填上面拿到的token</trt:ProfileToken> </trt:GetStreamUri>第一次写容易踩的坑有两个。第一是SOAPAction大小写和命名空间版本,部分设备对Action很严格,写错直接返回Fault。命名空间有ver10和ver20两套,老设备和兼容性差的设备只认ver10,我建议第一版统一用ver10,遇到不支持的再按版本升级。第二是响应的XML里,不同厂家可能返回不同的namespace前缀,但XmlPullParser按本地名解析能绕开这个问题。
认证方面,Onvif官方推荐WS-Security的UsernameToken Digest,要算Nonce、Created、PasswordDigest三个值再塞进Header,手写比较繁琐。我的经验是:先用无认证模式测通一遍,设备返回401或Soap Fault再补认证。很多摄像头默认是关鉴权的,或者兼容HTTP Basic Auth,这两种情况都不需要完整的WS-Security。真正强制要求Digest的设备确实存在,不过占比不高。
3.3 拼出最终播放地址
GetStreamUri返回的Uri可能是rtsp://192.168.1.64:554/Streaming/Channels/101,但多数设备不把用户名密码写进Uri。如果设备开了RTSP鉴权,你得手动拼接:
String user = "admin"; String pass = "yourpass"; String rawUri = "rtsp://192.168.1.64:554/Streaming/Channels/101"; String finalUri = rawUri.replace("rtsp://", "rtsp://" + Uri.encode(user) + ":" + Uri.encode(pass) + "@");这个拼接看着简单,容易出错的是密码里有@、:、/这些特殊字符时不做URL编码。我见过一个项目因为密码里带@符号,RTSP地址被拼错,排查了一整天才发现是编码问题。所以每个字段都要单独Uri.encode再拼。
4. Android端绕不开的几个特殊处理
4.1 权限与MulticastLock的实现要点
Manifest里这几个权限一个都不能少:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>如果还要顺带获取WiFi信息或者做网络状态判断,Android 8.0以上还需要定位权限,因为系统把扫描WiFi当成地理位置相关操作。这个容易被忽略,运行时权限弹窗不授予,WifiManager相关API会返回空。
MulticastLock的获取和释放要跟后台任务生命周期绑到一起,不要在Activity的onCreate和onDestroy里直接配一对。发现流程往往是异步的,Activity退出时流程可能还在跑,锁提前释放后后续响应全部收不到。我是在启动发现任务时acquire,任务执行完的finally里release,这样即使异常退出也不会把锁一直攥着。
4.2 Android 9+明文HTTP限制
Onvif走的是http://不是https://,Android 9开始默认禁止明文流量,OkHttp会直接报Cleartext HTTP traffic not permitted。开发期最快的方式是给application加开关:
<application android:usesCleartextTraffic="true" ...>如果产品要求更高,可以配置networkSecurityConfig对特定网段放行。但做局域网硬件工具类App,全局允许明文流量的代价不大,因为流量本来就不出局域网。我自己的项目开发阶段直接usesCleartextTraffic="true",上线时再按安全要求收紧。
线程模型上,UDP收发、SOAP请求都是阻塞操作,不能跑在主线程。我一般用一个单线程的ExecutorService串行执行整个发现流程,遇到多设备再换固定线程数3到5的线程池,避免同一时间一堆请求打到低端摄像头上。
4.3 播放器选型与RTSP认证
拿到合法RTSP地址之后,Android原生MediaPlayer已经不太能指望了,实际项目基本两个选择。
一个是Media3 ExoPlayer,需要单独引入RTSP模块,比如media3-exoplayer-rtsp,然后直接MediaItem.fromUri(rtspUri)就能播。RTSP的Basic和Digest认证它都支持,前提是你把用户名密码拼进URL。另一个是LibVLC,RTSP兼容性覆盖面广,再奇葩的摄像头流基本都能播,代价是APK体积变大不少,API风格也偏老。
个人排序是先试ExoPlayer,遇到播不了的奇葩流再上LibVLC。实际踩过的一个坑是:某些摄像头RTSP强制走TCP而不是UDP,ExoPlayer对强制TCP传输的支持不如VLC,这种情况下就要查看设备的RTSP传输配置,或者直接换LibVLC拉流。
5. 实测多品牌设备后的兼容性笔记
5.1 Probe发出去了却收不到响应,先别怀疑代码
手机发了Probe但迟迟没有ProbeMatch,我建议按这个顺序排查:
- 手机和摄像头是不是同一网段,跨网段组播基本不通;
- MulticastLock拿没拿到,release是不是被上面的生命周期问题提前释放了;
- 用PC上的Onvif Device Manager在同一网络里点一下发现,如果PC也发现不了,说明设备侧Onvif没开或处于异常状态,不是App的问题;
- 最后再上抓包工具,看UDP包到底有没有到达设备。
设备发现类问题,超过一半是"手机没开组播锁"和"手机和摄像头不在同一局域网"两个原因。另有一小部分是厂家把摄像头出厂默认Onvif关闭,需要去设备Web管理页打开。
5.2 响应XML的命名空间差异
不同厂家对Onvif版本的支持参差不齐,有的返回ver10/media/wsdl,有的返回ver20,还有的连前缀都不一样。所以解析响应时必须用本地名解析,不要在前缀上做文章。发送请求时固定用ver10起步,设备报Fault再说。
ProbeMatch里XAddrs可能不止一个,有的设备会同时给LANIP和WLANIP两条地址,手机往往只能访问其中一条。我处理的办法是全部拆分出来,逐个用1到2秒超时去请求GetCapabilities,第一个成功的地址作为本次会话主用地址。这个逻辑看着笨,实际上是兼容性最好的做法。
5.3 多设备环境下的并发与去重
环境里有几十台摄像头时,Probe一发会收到一堆ProbeMatch,而且由于组播特性,同一设备的响应可能出现两三次。处理时必须做好去重,我用一个Set记录已处理过的XAddrs。后续的SOAP三连不要并发轰,低端摄像头扛不住。我是用固定线程数3到5的线程池串行处理设备列表,每台设备之间留200毫秒间隔,实测比全并发稳定得多。
每个设备的发现会话最好设置总超时。发现阶段设6秒,SOAP三连每步3秒,超时的设备先标记为"不支持Onvif"不再重试,避免越积越多拖垮整个列表刷新。这个超时值不是拍脑袋定的,是拿十来种设备实测出来的折中值,太短会漏掉慢速设备,太长会让用户感觉列表一直转圈。
5.4 排查问题时的抓包手段
PC上Wireshark抓包是终极大杀器。把手机、摄像头、PC接到同一个交换机,在PC上抓3702端口的UDP包,能直接看到Probe是否到达、设备是否回了ProbeMatch、SOAP请求的HTTP状态码是200还是401。这类问题靠猜效率太低,抓包看响应是最省时间的。
常见现象和排查方向我整理成了表:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| Probe无任何响应 | 未持有MulticastLock / 跨网段 / 设备Onvif未开启 | 锁生命周期检查;PC端Onvif Device Manager对比验证 |
| ProbeMatch有地址,GetCapabilities超时 | XAddrs里给的是设备不可达地址 | 遍历XAddrs所有地址逐个试 |
| GetProfiles返回空列表 | 设备不支持标准媒体Profile | 换GetVideoSources兜底 |
| GetStreamUri返回401 | 设备开启认证,请求没带 | 拼接用户名密码,或实现WS-Security Digest |
| 拿到地址ExoPlayer却播不了 | 设备RTSP要求TCP传输 / 厂家私有路径 | 改播放器传输模式,或用LibVLC验证 |
如果条件允许,先在PC的Onvif Device Manager里把摄像头完整流程走一遍,确认设备本身没问题,再回到Android上定位。很多时候差距只在认证方式,ODM会自动带上WS-Security,而你的App还没实现,看到ODM正常、App失败,优先怀疑认证环节。
最后分享一个经验:拿到RTSP地址只是第一步,真正上线前一定要做设备自动重连、断流恢复、地址过期重取这几个兜底逻辑。Onvif的地址理论上是会话级的,设备重启后旧地址可能失效,到时候重新走一遍三连流程比让用户重启App体验好得多。这套链路跑顺之后,你会发现局域网摄像头接入这件事,跟品牌的关系真的不大。
本文还有配套的精品资源,点击获取