Android端实现Onvif IPC开发(二)——在Andro
Android端实现Onvif IPC开发:
《Android端实现Onvif IPC开发(一)——gSoap移植NDK尝试》
《Android端实现Onvif IPC开发(二)——在Android端搭建服务器模拟Onvif IP Camera》
《Android端实现Onvif IPC开发(三)——在Android端搭建RTSP服务器(更新中...)》
《Android端实现Onvif IPC开发(四)——Android编码ColorFormat解析及常见格式直接转换(更新中...)》
《Android端实现Onvif IPC开发(五)——H264,H265硬编码及RTP分片传输(更新中...)》
本篇内容简介:
此处主要借鉴的项目有:https://github.com/fyhertz/spydroid-ipcamera
由于搞这个项目时,参考并阅读了许多资料,可能存在相似却未声明的借鉴之处,请联系我修改或声明
本篇是上一文章移植失败采取的第二方案,通过在android搭建service,模拟成一个onvif协议对接的IPC端,在这之前,首先需要明白,onvif设备对接的流程或者说方式,接下来的文章内容也是基于下面一条流程去实现。
- 发现-->请求-->控制-->打开视频预览
一、作为Server端实现被发现功能
IPC设备基于Onvif被发现,首先要明白 WS-Discovery: 动态的探测可用服务并调用之
- 这一功能的原理是,在同一网段中维持一个固定地址值的UDP广播,以特定的xml指令进行请求和响应,即完成设备端的信息查询和识别
- IPC固定地址值:239.255.255.250 端口:3702,服务端的这个广播地址是固定的
- 流程是:client端发送Probe请求,server根据Probe请求响应对应的ProbeMatch返回供client端识别
- 请求和返回数据可以通过抓包去查看模拟
- 在android端实现Onvif IPC功能的软件几乎没有,我找了很久才找到一个国外实现的项目,感兴趣的可以下载 :AndroidIPC_apk
接下来是具体实现
注意点:
-
此处为demo,在项目中最好将这个广播添加到Android service中进行
-
android需要使用组播MulticastSocket实现udp搜索功能,此处需要权限:
// 允许应用程序访问WIFI网卡的网络信息 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> // 允许应用程序访问有关的网络信息 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> //android 组播功能权限 <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
-
回传的值可以通过在assets中写好,通过%s代传值去方便实现,以下为简便写死的
-
获取服务端IP地址:
private String getlocalip() { WifiManager wifiManager = (WifiManager) mApplication.getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiInfo info = wifiManager.getConnectionInfo(); String ipaddress = null; if (info != null && info.getNetworkId() > -1) { int i = info.getIpAddress(); ipaddress = String.format(Locale.ENGLISH, "%d.%d.%d.%d", i & 0xff, i >> 8 & 0xff, i >> 16 & 0xff, i >> 24 & 0xff); return ipaddress; } else if ((ipaddress = Utilities.getLocalIpAddress(true)) != null) { return ipaddress; } return "no found"; }
-
初始化数据:此处用于返回对应的服务端IP地址值和端口,service需要返回的url,USER_NAME和USER_PASSWORD用于鉴权使用,这边写死了
private void initData() { serverIp = getlocalip(); Log.e("ipserver", "IP addresss:" + serverIp); devicesBack = mApplication.getDevicesBack(); devicesBack.setIpAddress(serverIp); devicesBack.setProt(serverPort); devicesBack.setUserName(DevicesInfo.USER_NAME); devicesBack.setPsw(DevicesInfo.USER_PASSWORD); devicesBack.setServiceUrl("http://" + serverIp + ":8080/onvif/device_service"); mApplication.setDevicesBackBean(devicesBack); Log.e("Description", "setDevicesBackBean 1: " + devicesBack.toString()); } public class DevicesBackBean { /** * 用户名/密码 */ private String userName; public String getEncodertype() { return encodertype; } public void setEncodertype(String encodertype) { this.encodertype = encodertype; } private String encodertype; private String psw; //IP地址 private String ipAddress; /** * serviceUrl,uuid 通过广播包搜索设备获取 */ private String serviceUrl; private String uuid; /** * getCapabilities */ private String mediaUrl; private String ptzUrl; private String imageUrl; private String eventUrl; private String analyticsUrl; private String source_with = "1920"; private String source_height = "1080"; private String encoder_with = "1920"; private String encoder_height = "1080"; private String frameRateLimit = "25"; private String bitrateLimit = "10000"; private String prot = "8080"; private String media_timeout = "PT30S"; public String getRtsp_port() { return rtsp_port; } public void setRtsp_port(String rtsp_port) { this.rtsp_port = rtsp_port; } private String rtsp_port = "8086"; public String getRtsp_stream() { return rtsp_stream; } public void setRtsp_stream(String rtsp_stream) { this.rtsp_stream = rtsp_stream; } private String rtsp_stream = "";//---/main.h264 public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPsw() { return psw; } public void setPsw(String psw) { this.psw = psw; } public String getIpAddress() { return ipAddress; } public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } public String getServiceUrl() { return serviceUrl; } public void setServiceUrl(String serviceUrl) { this.serviceUrl = serviceUrl; } public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public String getMediaUrl() { return mediaUrl; } public void setMediaUrl(String mediaUrl) { this.mediaUrl = mediaUrl; } public String getPtzUrl() { return ptzUrl; } public void setPtzUrl(String ptzUrl) { this.ptzUrl = ptzUrl; } public String getImageUrl() { return imageUrl; } public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } public String getEventUrl() { return eventUrl; } public void setEventUrl(String eventUrl) { this.eventUrl = eventUrl; } public String getAnalyticsUrl() { return analyticsUrl; } public void setAnalyticsUrl(String analyticsUrl) { this.analyticsUrl = analyticsUrl; } public String getSource_with() { return source_with; } public void setSource_with(String source_with) { this.source_with = source_with; } public String getSource_height() { return source_height; } public void setSource_height(String source_height) { this.source_height = source_height; } public String getEncoder_with() { return encoder_with; } public void setEncoder_with(String encoder_with) { this.encoder_with = encoder_with; } public String getEncoder_height() { return encoder_height; } public void setEncoder_height(String encoder_height) { this.encoder_height = encoder_height; } public String getFrameRateLimit() { return frameRateLimit; } public void setFrameRateLimit(String frameRateLimit) { this.frameRateLimit = frameRateLimit; } public String getBitrateLimit() { return bitrateLimit; } public void setBitrateLimit(String bitrateLimit) { this.bitrateLimit = bitrateLimit; } public String getProt() { return prot; } public void setProt(String prot) { this.prot = prot; } public String getMedia_timeout() { return media_timeout; } public void setMedia_timeout(String media_timeout) { this.media_timeout = media_timeout; } @Override public String toString() { return "DevicesBackBean{" + "userName='" + userName + '\'' + ", psw='" + psw + '\'' + ", ipAddress='" + ipAddress + '\'' + ", serviceUrl='" + serviceUrl + '\'' + ", uuid='" + uuid + '\'' + ", mediaUrl='" + mediaUrl + '\'' + ", ptzUrl='" + ptzUrl + '\'' + ", imageUrl='" + imageUrl + '\'' + ", eventUrl='" + eventUrl + '\'' + ", analyticsUrl='" + analyticsUrl + '\'' + ", source_with='" + source_with + '\'' + ", source_height='" + source_height + '\'' + ", encoder_with='" + encoder_with + '\'' + ", encoder_height='" + encoder_height + '\'' + ", frameRateLimit='" + frameRateLimit + '\'' + ", bitrateLimit='" + bitrateLimit + '\'' + ", prot='" + prot + '\'' + ", media_timeout='" + media_timeout + '\'' + '}';}} public class DevicesInfo { public static final String USER_NAME = "***"; public static final String USER_PASSWORD = "***"; public static final String GET_MEDIA = "/onvif/Media"; public static final String GET_PTZ = "/onvif/PTZ"; public static final String GET_ANALYTICS = "/onvif/Analytics"; public static final String GET_DEVICE_SERVICE = "/onvif/device_service"; public static final String GET_EVENTS = "/onvif/Events"; public static final String GET_IMAGING = "/onvif/Imaging"; // public static final String GET_StreamUri = ""; // public static final String USER_NAME=""; }
-
线程实现:
class UdpB extends Thread { @Override public void run() { MulticastSocket socket = null; InetAddress address = null; try { socket = new MulticastSocket(3702); address = InetAddress.getByName("239.255.255.250"); socket.joinGroup(address); DatagramPacket packet; Log.e(TAG, "receiver packet"); // 接收数据 byte[] rev = new byte[1024 * 6]; while (flag) { packet = new DatagramPacket(rev, rev.length); socket.receive(packet); String receiver = new String(packet.getData()).trim(); //不加trim,则会打印出512个byte,后面是乱码 Log.e(TAG, "get data = " + receiver); Log.e(TAG, "socket = " + socket.getInetAddress() + " " + socket.getLocalSocketAddress() + " " + socket.getLocalAddress() + " " + socket.getPort()); Log.e(TAG, "socket = " + packet.getAddress() + " " + packet.getSocketAddress() + " " + packet.getPort() + " "); //解析Probe请求信息,此处主要要提取client端的uuid用于返回验证 DiscoveryReqHeader discoveryReqHeader = OnvifXmlResolver.getProbeResponse(receiver).getDiscoveryReqHeader(); String reqUuid = discoveryReqHeader.getaMessageId(); String a_action = discoveryReqHeader.getaAction(); Log.e(TAG, "reqUuid: " + reqUuid + " a_action: " + a_action); if (receiver.contains("Envelope")) { devicesBack = mApplication.getDevicesBack(); devicesBack.setUuid(reqUuid); mApplication.setDevicesBackBean(devicesBack); Log.e(TAG, "setDevicesBackBean: " + devicesBack.toString()); //发送数据包 //返回对应的ProbeMatch String sendBack = Utilities.generateDeviceProbeMatch(reqUuid, serverIp, Utilities.getUrnUuid(getApplicationContext()), Utilities .getMessageId()); byte[] buf = sendBack.getBytes(); Log.e(TAG, "send packet: " + sendBack); packet = new DatagramPacket(buf, buf.length, packet.getAddress(), packet.getPort()); socket.send(packet); } } } catch (IOException e) { mHander.sendEmptyMessage(111); } //退出组播 try { socket.leaveGroup(address); socket.close(); } catch (IOException e) { mHander.sendEmptyMessage(111); }}}
-
-
返回ProbeMatch
/** * generate a soap request for probe onvif device */ public static String generateDeviceProbeMatch(String uuid_req, String address_local, String urn_uuid, String messageId) { // if(!ObjectCheck.validString(uuid)) { // return ""; // } StringBuffer sb; sb = new StringBuffer(); sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n"); sb.append("<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:SOAP-ENC=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:xsi=\"http://www" + ".w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" " + "xmlns:wsdd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:dn=\"http://www.onvif" + ".org/ver10/network/wsdl\">\r\n"); sb.append(" <SOAP-ENV:Header>\r\n"); sb.append(" <wsa:MessageID>urn:uuid" + urn_uuid + "</wsa:MessageID>\r\n"); sb.append(" <wsa:RelatesTo>" + uuid_req + "</wsa:RelatesTo>\r\n"); sb.append(" <wsa:ReplyTo SOAP-ENV:mustUnderstand=\"true\">\r\n"); sb.append(" <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>\r\n"); sb.append(" </wsa:ReplyTo>\r\n"); sb.append(" <wsa:To SOAP-ENV:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>\r\n"); sb.append(" <wsa:Action SOAP-ENV:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches</wsa:Action>\r\n"); sb.append(" <wsdd:AppSequence InstanceId=\"0\" MessageNumber=\"5\"></wsdd:AppSequence>\r\n"); sb.append(" </SOAP-ENV:Header>\r\n"); sb.append(" <SOAP-ENV:Body>\r\n"); sb.append(" <wsdd:ProbeMatches>\r\n"); sb.append(" <wsdd:ProbeMatch>\r\n"); sb.append(" <wsa:EndpointReference>\r\n"); sb.append(" <wsa:Address>urn:uuid:" + urn_uuid + "</wsa:Address>\r\n"); sb.append(" </wsa:EndpointReference>\r\n"); sb.append(" <wsdd:Types>dn:NetworkVideoTransmitter</wsdd:Types>\r\n"); sb.append(" <wsdd:Scopes>onvif://www.onvif.org/Profile/Streaming onvif://www.onvif.org/type/video_encoder onvif://www.onvif" + ".org/type/audio_encoder onvif://www.onvif.org/hardware/ONVIF-Emu onvif://www.onvif.org/name/ONVIF-Emu onvif://www.onvif.org/location/Default</wsdd:Scopes> \r\n"); sb.append(" <wsdd:XAddrs>http://" + address_local + ":8080/onvif/device_service</wsdd:XAddrs>\r\n"); sb.append(" <wsdd:MetadataVersion>10</wsdd:MetadataVersion>\r\n"); sb.append(" </wsdd:ProbeMatch>\r\n"); sb.append(" </wsdd:ProbeMatches>\r\n"); sb.append(" </SOAP-ENV:Body>\r\n"); sb.append("</SOAP-ENV:Envelope>"); return sb.toString(); }
-
请求的Probe抓包:
<?xml version="1.0" encoding="utf-8"?> <Envelope xmlns="http://www.w3.org/2003/05/soap-envelope" xmlns:tds="http://www.onvif.org/ver10/device/wsdl"> <Header> <wsa:MessageID xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">uuid:d9305f63-5027-4edb-b1f1-58c463b5419a</wsa:MessageID> <wsa:To xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To> <wsa:Action xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action> </Header> <Body> <Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Types>tds:Device</Types> <Scopes/> </Probe> </Body> </Envelope>
-
其中OnvifXmlResolver为xml解析,感兴趣的可以看我的文章: SAX解析XML文件,通过解析相应节点去获取需要的内容,以下都会有用到,这个代码会在下一篇文章总给出
-
完成以上内容既可以被搜索到了,干净用工具测试一下,发现OK!
-
二、在Android上搭建一个Server用于接收和响应Client请求####
服务端这边主要由开源框架spydroid-ipcamera改动得来,项目地址在文章头部给出,读者可以下载阅读,其实改起来很简单,下面先对该项目简单分析:
在阅读下面分析时,最好在下载该项目,进行简单阅读或对比阅读,这样会更明了,我本人在写博客时更提倡读者自己动手,而不是直接给个现成的demo
这个工程只要有大体的实现思维,改动起来其实很容易
分析spydroid-ipcamera实现:
- 该项目分别搭建了rstp server和一个本地的http web server,我们这一篇文章主要分析CustomHttpServer和改动这个类
-
CustomHttpServer继承TinyHttpServer类(简便封装的Server端,基于org.apache.http框架),注意此处,我们在使用的时候,往往会出现V4包版本冲突的问题,这是由于google在新版本的v4包不再使用这个框架的缘故,故此我们只要在app gradle中声明
android { useLibrary 'org.apache.http.legacy' }
-
在TinyHttpServer中,方法addRequestHandler,学过java web的都知道,此处为解析请求内容的结构体
//在TinyHttpServer中: /** * You may add some HttpRequestHandler to modify the default behavior of the server. * @param pattern Patterns may have three formats: * or *<uri> or <uri>* * @param handler A HttpRequestHandler */ protected void addRequestHandler(String pattern, HttpRequestHandler handler) { mRegistry.register(pattern, handler); } //在CustomHttpServer中: @Override public void onCreate() { super.onCreate(); mDescriptionRequestHandler = new DescriptionRequestHandler(); addRequestHandler("/spydroid.sdp*", mDescriptionRequestHandler);//.sdp请求 addRequestHandler("/request.json*", new CustomRequestHandler());//此处为json请求,这个搞android的应该都会,不做简述 } //.sdp请求: SDP会话描述协议:为会话通知、会话邀请和其它形式的多媒体会话初始化等目的提供了多媒体会话描述。 会话目录用于协助多媒体会议的通告,并为会话参与者传送相关设置信息。 SDP 即用于将这种信息传输到接收端。 SDP 完全是一种会话描述格式――它不属于传输协议 ――它只使用不同的适当的传输协议,包括会话通知协议 (SAP) 、会话初始协议(SIP) 、实时流协议 (RTSP)、 MIME 扩展协议的电子邮件以及超文本传输协议 (HTTP)。SDP 的设计宗旨是通用性, 它可以应用于大范围的网络环境和应用程序,而不仅仅局限于组播会话目录
-
在DescriptionRequestHandler处理网络 request 和 response, request我们在android中经常写,现在让我们来写一次response把
/** * Allows to start streams (a session contains one or more streams) from the HTTP server by requesting * this URL: http://ip/spydroid.sdp (the RTSP server is not needed here). **/ class DescriptionRequestHandler implements HttpRequestHandler { private final SessionInfo[] mSessionList = new SessionInfo[MAX_STREAM_NUM]; private class SessionInfo { public Session session; public String uri; public String description; } public DescriptionRequestHandler() { for (int i=0;i<MAX_STREAM_NUM;i++) { mSessionList[i] = new SessionInfo(); } } public synchronized void handle(HttpRequest request, HttpResponse response, HttpContext context) throws HttpException { Socket socket = ((TinyHttpServer.MHttpContext)context).getSocket(); String uri = request.getRequestLine().getUri(); int id = 0; boolean stop = false; try { // A stream id can be specified in the URI, this id is associated to a session List<NameValuePair> params = URLEncodedUtils.parse(URI.create(uri),"UTF-8"); uri = ""; if (params.size()>0) { for (Iterator<NameValuePair> it = params.iterator();it.hasNext();) { NameValuePair param = it.next(); if (param.getName().equalsIgnoreCase("id")) { try { id = Integer.parseInt(param.getValue()); } catch (Exception ignore) {} } else if (param.getName().equalsIgnoreCase("stop")) { stop = true; } } } params.remove("id"); uri = "http://c?" + URLEncodedUtils.format(params, "UTF-8"); if (!uri.equals(mSessionList[id].uri)) { mSessionList[id].uri = uri; // Stops all streams if a Session already exists if (mSessionList[id].session != null) { boolean streaming = isStreaming(); mSessionList[id].session.syncStop(); if (streaming && !isStreaming()) { postMessage(MESSAGE_STREAMING_STOPPED); } mSessionList[id].session.release(); mSessionList[id].session = null; } if (!stop) { boolean b = false; if (mSessionList[id].session != null) { InetAddress dest = InetAddress.getByName(mSessionList[id].session.getDestination()); if (!dest.isMulticastAddress()) { b = true; } } if (mSessionList[id].session == null || b) { // Parses URI and creates the Session mSessionList[id].session = UriParser.parse(uri); mSessions.put(mSessionList[id].session, null); } // Sets proper origin & dest mSessionList[id].session.setOrigin(socket.getLocalAddress().getHostAddress()); if (mSessionList[id].session.getDestination()==null) { mSessionList[id].session.setDestination(socket.getInetAddress().getHostAddress()); } // Starts all streams associated to the Session boolean streaming = isStreaming(); mSessionList[id].session.syncStart(); if (!streaming && isStreaming()) { postMessage(MESSAGE_STREAMING_STARTED); } mSessionList[id].description = mSessionList[id].session.getSessionDescription().replace("Unnamed", "Stream-"+id); Log.v(TAG, mSessionList[id].description); } } final int fid = id; final boolean fstop = stop; response.setStatusCode(HttpStatus.SC_OK); EntityTemplate body = new EntityTemplate(new ContentProducer() { public void writeTo(final OutputStream outstream) throws IOException { OutputStreamWriter writer = new OutputStreamWriter(outstream, "UTF-8"); if (!fstop) { writer.write(mSessionList[fid].description); } else { writer.write("STOPPED"); } writer.flush(); } }); body.setContentType("application/sdp; charset=UTF-8"); response.setEntity(body); } catch (Exception e) { mSessionList[id].uri = ""; response.setStatusCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); Log.e(TAG,e.getMessage()!=null?e.getMessage():"An unknown error occurred"); e.printStackTrace(); postError(e,ERROR_START_FAILED); } } }
-
在这里CustomHttpServer和CustomRtspServer,都是基于android的service,运行方法也一样:
//1. 启动服务,别忘了在清单文件中声明 this.startService(new Intent(this,CustomHttpServer.class)); //2. bindService bindService(new Intent(this,CustomHttpServer.class), mHttpServiceConnection, Context.BIND_AUTO_CREATE); //3. unbindService if (mHttpServer != null) mHttpServer.removeCallbackListener(mHttpCallbackListener); unbindService(mHttpServiceConnection); //// Kills HTTP server this.stopService(new Intent(this,CustomHttpServer.class)); private ServiceConnection mHttpServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mHttpServer = (CustomHttpServer) ((TinyHttpServer.LocalBinder)service).getService(); mHttpServer.addCallbackListener(mHttpCallbackListener); mHttpServer.start(); } @Override public void onServiceDisconnected(ComponentName name) {} }; //在callback中处理相应的View显示和刷新 private TinyHttpServer.CallbackListener mHttpCallbackListener = new TinyHttpServer.CallbackListener() { @Override public void onError(TinyHttpServer server, Exception e, int error) { // We alert the user that the port is already used by another app. if (error == TinyHttpServer.ERROR_HTTP_BIND_FAILED || error == TinyHttpServer.ERROR_HTTPS_BIND_FAILED) { String str = error==TinyHttpServer.ERROR_HTTP_BIND_FAILED?"HTTP":"HTTPS"; new AlertDialog.Builder(SpydroidActivity.this) .setTitle(R.string.port_used) .setMessage(getString(R.string.bind_failed, str)) .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(final DialogInterface dialog, final int id) { startActivityForResult(new Intent(SpydroidActivity.this, OptionsActivity.class),0); } }) .show(); } } @Override public void onMessage(TinyHttpServer server, int message) { if (message==CustomHttpServer.MESSAGE_STREAMING_STARTED) { if (mAdapter != null && mAdapter.getHandsetFragment() != null) mAdapter.getHandsetFragment().update(); if (mAdapter != null && mAdapter.getPreviewFragment() != null) mAdapter.getPreviewFragment().update(); } else if (message==CustomHttpServer.MESSAGE_STREAMING_STOPPED) { if (mAdapter != null && mAdapter.getHandsetFragment() != null) mAdapter.getHandsetFragment().update(); if (mAdapter != null && mAdapter.getPreviewFragment() != null) mAdapter.getPreviewFragment().update(); } } };
-
接下来实现我们的Onvif Server:
首先选取我们要用的,TinyHttpServer是我们需要的,参照CustomHttpServer写一个OnvifHttpServer,其中web访问我们不做,可以去掉
-
自定义一个OnvifHttpServer集成TinyHttpServer,对照CustomHttpServer实现相应方法,Onvif Client请求端请求的方式是post,请求方式我们就假装不知道,我们定义Request pattern为所有格式即可,即: "/*"
@Override public void onCreate() { super.onCreate(); mDescriptionRequestHandler = new DescriptionRequestHandler(); mDescriptionOnvifHandler = new DescriptionOnvifHandler(); addRequestHandler("/*", mDescriptionOnvifHandler); addRequestHandler("/request.json*", new CustomRequestHandler()); }
-
实现基于Onvif的请求解析DescriptionRequestHandler,返回的格式为:"application/soap+xml; charset=UTF-8",接口有很多,要想实现标准工具播放,要实现很多很多的接口,下面简单介绍几个,感兴趣的可以自己去抓包标准设备完成:
-
GetDeviceInformation:获取设备信息
-
GetCapabilities :获取设备性能
-
GetProfiles获取设备权限
-
GetStreamUri 获取设备流媒体服务地址和相应信息
/** * this URL for onvif request. **/ class DescriptionOnvifHandler implements HttpRequestHandler { private final IpcServerApplication mApplication; public DescriptionOnvifHandler() { mApplication = (IpcServerApplication) getApplication(); } public void handle(HttpRequest request, HttpResponse response, HttpContext arg2) throws HttpException, IOException { if (request.getRequestLine().getMethod().equals("POST")) {//onvif请求为post // Retrieve the POST content final String url = URLDecoder.decode(request.getRequestLine().getUri()); Log.e(TAG, "DescriptionRequestHandler------------------:url " + url); HttpEntityEnclosingRequest post = (HttpEntityEnclosingRequest) request; byte[] entityContent = EntityUtils.toByteArray(post.getEntity()); String content = new String(entityContent, Charset.forName("UTF-8")); //此处需要用到响应的你想给请求设备的信息,我作为一个全局的bean类保存,此处可以实现为序列化到本地或者sp保存都行 DevicesBackBean devicesBack = mApplication.getDevicesBack(); LogUtils.e(TAG, "DescriptionRequestHandler :" + devicesBack.toString()); String backS = null; //接下来就是返回请求了,这里可以解析请求的包,即XML数据,判断相应为什么请求,这个地方就体现了,模拟Onvif ipc //的尿性之处,请求接口相当繁多复杂,蛋疼的一匹,这样实现终非正道,如果要完成一个标识的Onvif IPC端还是要采取我第 //一篇文章的移植才是正轨,这里简要实现几个接口,作为参考,大家有问题可以私信我一起沟通 if (content.contains("GetDeviceInformation")) { backS = Utilities.getPostString("GetDeviceInformationReturn.xml", false, getApplicationContext(), mApplication.getDevicesBack()); LogUtils.e(TAG, "DescriptionRequestHandler :" + mApplication.getDevicesBack().toString()); } else if (content.contains("GetProfiles")) {//需要鉴权 if (needProfiles) { boolean isAuthTrue = DigestUtils.doAuthBack(content); if (!isAuthTrue) { doBackFail(response); return; } } backS = Utilities.getPostString("getProfilesReturn.xml", true, getApplicationContext(), mApplication.getDevicesBack()); LogUtils.e("DescriptionRequestHandler", "-------getProfilesReturn--" + backS); }else { doBackFail(response); return; } LogUtils.e(TAG, "getProfilesReturn backS:" + backS); // Return the response final String finalBackS = backS; ByteArrayEntity body = new ByteArrayEntity(finalBackS.getBytes()); ByteArrayInputStream is = (ByteArrayInputStream) body.getContent(); response.setStatusCode(HttpStatus.SC_OK); body.setContentType("application/soap+xml; charset=UTF-8"); response.setEntity(body); } } }
-
-
下面给一个我抓包的请求:
-
getProfiles和getProfilesBack:
<?xml version="1.0" encoding="utf-8"?> <s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"> <s:Header> <Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1"> <UsernameToken> <Username>%s</Username> <Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">%s</Password> <Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">%s</Nonce> <Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">%s</Created> </UsernameToken> </Security> </s:Header> <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <GetProfile xmlns="http://www.onvif.org/ver10/media/wsdl"> <ProfileToken>%s</ProfileToken> </GetProfile> </s:Body> </s:Envelope> <?xml version="1.0" encoding="utf-8" ?> <SOAP-ENV:Envelope xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema"> <SOAP-ENV:Body><trt:GetProfilesResponse><trt:Profiles fixed="true" token="Profile1"> <tt:Name>Profile1</tt:Name> <tt:VideoSourceConfiguration token="VideoSourceToken"> <tt:Name>VideoSourceConfig</tt:Name><tt:UseCount>1</tt:UseCount><tt:SourceToken>VideoSource_1</tt:SourceToken><tt:Bounds height="%s" width="%s" x="0" y="0"/> </tt:VideoSourceConfiguration> <tt:VideoEncoderConfiguration token="VideoEncoderToken_1"> <tt:Name>VideoEncoder_1</tt:Name><tt:UseCount>1</tt:UseCount><tt:Encoding>%s</tt:Encoding><tt:Resolution> <tt:Width>%s</tt:Width><tt:Height>%s</tt:Height> </tt:Resolution> <tt:Quality>44.0</tt:Quality> <tt:RateControl><tt:FrameRateLimit>%s</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>%s</tt:BitrateLimit> </tt:RateControl> <tt:H264><tt:GovLength>100</tt:GovLength><tt:H264Profile>Baseline</tt:H264Profile> </tt:H264> <tt:Multicast> <tt:Address> <tt:Type>IPv4</tt:Type><tt:IPv4Address>0.0.0.0</tt:IPv4Address><tt:IPv6Address /> </tt:Address> <tt:Port>0</tt:Port> <tt:TTL>0</tt:TTL> <tt:AutoStart>false</tt:AutoStart> </tt:Multicast> <tt:SessionTimeout>PT30S</tt:SessionTimeout> </tt:VideoEncoderConfiguration> <tt:PTZConfiguration token="PTZToken"> <tt:Name>PTZ</tt:Name> <tt:UseCount>1</tt:UseCount> <tt:NodeToken>PTZNODETOKEN</tt:NodeToken> <tt:DefaultAbsolutePantTiltPositionSpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace</tt:DefaultAbsolutePantTiltPositionSpace> <tt:DefaultAbsoluteZoomPositionSpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace</tt:DefaultAbsoluteZoomPositionSpace> <tt:DefaultRelativePanTiltTranslationSpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace</tt:DefaultRelativePanTiltTranslationSpace> <tt:DefaultRelativeZoomTranslationSpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace</tt:DefaultRelativeZoomTranslationSpace> <tt:DefaultContinuousPanTiltVelocitySpace>http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace</tt:DefaultContinuousPanTiltVelocitySpace> <tt:DefaultContinuousZoomVelocitySpace>http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace</tt:DefaultContinuousZoomVelocitySpace> <tt:DefaultPTZSpeed> <tt:PanTilt space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace" x="0.100000" y="0.100000"/> <tt:Zoom space="http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace" x="1.000000"/> </tt:DefaultPTZSpeed> <tt:DefaultPTZTimeout>PT1S</tt:DefaultPTZTimeout> <tt:PanTiltLimits> <tt:Range> <tt:URI>http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace</tt:URI> <tt:XRange> <tt:Min>-INF</tt:Min><tt:Max>INF</tt:Max> </tt:XRange> <tt:YRange> <tt:Min>-INF</tt:Min><tt:Max>INF</tt:Max> </tt:YRange> </tt:Range> </tt:PanTiltLimits> <tt:ZoomLimits> <tt:Range> <tt:URI>http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace</tt:URI> <tt:XRange> <tt:Min>-INF</tt:Min> <tt:Max>INF</tt:Max> </tt:XRange> </tt:Range> </tt:ZoomLimits> </tt:PTZConfiguration> </trt:Profiles> </trt:GetProfilesResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
-
-
返回端给出的代码只需要把相应的需要返回的信息替代到里面字符串即可
-
关于鉴权方面,Onvif的鉴权有2中,WS-username token和Digest,可参考文章:Onvif协议及其在Android下的实现官方格式为
Digest=B64Encode(SHA1(B64ENCODE(Nonce)+Date+Password)) //nonce只是一个16位随机数即可 //Sha-1: MessageDigest md = MessageDigest.getInstance("SHA-1"); //date:参考值:"2013-09-17T09:13:35Z"; 由客户端请求给出 //此处我的不便摘要,详细可参考文章:https://blog.csdn.net/yanjiee/article/details/18809107 public String getPasswordEncode(String nonce, String password, String date) { try { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] b1 = Base64.decode(nonce.getBytes(), Base64.DEFAULT); byte[] b2 = date.getBytes(); // "2013-09-17T09:13:35Z"; byte[] b3 = password.getBytes(); byte[] b4 = new byte[b1.length + b2.length + b3.length]; md.update(b1, 0, b1.length); md.update(b2, 0, b2.length); md.update(b3, 0, b3.length); b4 = md.digest(); String result = new String(Base64.encode(b4, Base64.DEFAULT)); return result.replace("\n", ""); } catch (Exception e) { e.printStackTrace(); return ""; } } public String getNonce() { String base = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < 24; i++) { int number = random.nextInt(base.length()); sb.append(base.charAt(number)); } return sb.toString(); } private void createAuthString() { SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.CHINA); mCreated = df.format(new Date()); mNonce = getNonce(); mAuthPwd = getPasswordEncode(mNonce, mCamera.password, mCreated); }
三、当当当当当!,完成上面的服务框架搭建,启动服务
可通过标准工具检查到我们的设备啦,并且还能查询到响应的信息
- 其中EP Address即在文章初始,udp返回中的getUrnUuid,为我根据android 特定机器码生成的一个特定唯一的UUID
onvif_2.png