@[toc]
Android端實現(xiàn)Onvif IPC開發(fā):
【Android音視頻】Onvif-IPC開發(fā)(一)——gSoap-onvif移植到Android》
【Android音視頻】Onvif-IPC開發(fā)(二)——JAVA版本onvif服務(wù)器構(gòu)建Onvif-IP-Camera
【Android音視頻】Onvif IPC開發(fā)(三)——YUV格式深入淺出
[【Android音視頻】Onvif IPC開發(fā)(四)——Onvif移植Android架構(gòu)與補全方案(更新中...) ]
本篇內(nèi)容簡介:
由于搞這個項目時养涮,參考并閱讀了許多資料,可能存在相似卻未聲明的借鑒之處,請聯(lián)系我修改或聲明
本篇是上一文章移植失敗采取的第二方案猾普,通過在android搭建service惫叛,模擬成一個onvif協(xié)議對接的IPC端,在這之前,首先需要明白烘挫,onvif設(shè)備對接的流程或者說方式,接下來的文章內(nèi)容也是基于下面一條流程去實現(xiàn)柬甥。
- 發(fā)現(xiàn)-->請求-->控制-->打開視頻預(yù)覽
一饮六、作為Server端實現(xiàn)被發(fā)現(xiàn)功能
IPC設(shè)備基于Onvif被發(fā)現(xiàn)其垄,首先要明白 WS-Discovery: 動態(tài)的探測可用服務(wù)并調(diào)用之
- 這一功能的原理是,在同一網(wǎng)段中維持一個固定地址值的UDP廣播卤橄,以特定的xml指令進行請求和響應(yīng)绿满,即完成設(shè)備端的信息查詢和識別
- IPC固定地址值:239.255.255.250 端口:3702,服務(wù)端的這個廣播地址是固定的
- 流程是:client端發(fā)送Probe請求窟扑,server根據(jù)Probe請求響應(yīng)對應(yīng)的ProbeMatch返回供client端識別
- 請求和返回數(shù)據(jù)可以通過抓包去查看模擬
- 在android端實現(xiàn)Onvif IPC功能的軟件幾乎沒有棒口,我找了很久才找到一個國外實現(xiàn)的項目,感興趣的可以下載 :AndroidIPC_apk
具體實現(xiàn)
注意點:
此處為demo辜膝,在項目中最好將這個廣播添加到Android service中進行
-
android需要使用組播MulticastSocket實現(xiàn)udp搜索功能无牵,此處需要權(quán)限:
// 允許應(yīng)用程序訪問WIFI網(wǎng)卡的網(wǎng)絡(luò)信息 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> // 允許應(yīng)用程序訪問有關(guān)的網(wǎng)絡(luò)信息 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> //android 組播功能權(quán)限 <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
-
回傳的值可以通過在assets中寫好,通過%s代傳值去方便實現(xiàn)厂抖,以下為簡便寫死的
-
獲取服務(wù)端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"; }
-
初始化數(shù)據(jù):此處用于返回對應(yīng)的服務(wù)端IP地址值和端口茎毁,service需要返回的url,USER_NAME和USER_PASSWORD用于鑒權(quán)使用忱辅,這邊寫死了
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 通過廣播包搜索設(shè)備獲取 */ 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=""; }
-
線程實現(xiàn):
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"); // 接收數(shù)據(jù) 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()); //發(fā)送數(shù)據(jù)包 //返回對應(yīng)的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文件,通過解析相應(yīng)節(jié)點去獲取需要的內(nèi)容损搬,以下都會有用到碧库,這個代碼會在下一篇文章總給出
完成以上內(nèi)容既可以被搜索到了,干凈用工具測試一下巧勤,發(fā)現(xiàn)OK嵌灰!
-
二、在Android上搭建一個Server用于接收和響應(yīng)Client請求
服務(wù)端這邊主要由開源框架spydroid-ipcamera改動得來颅悉,項目地址在文章頭部給出沽瞭,讀者可以下載閱讀,其實改起來很簡單剩瓶,下面先對該項目簡單分析:
在閱讀下面分析時驹溃,最好在下載該項目,進行簡單閱讀或?qū)Ρ乳喿x延曙,這樣會更明了豌鹤,我本人在寫博客時更提倡讀者自己動手,而不是直接給個現(xiàn)成的demo
這個工程只要有大體的實現(xiàn)思維搂鲫,改動起來其實很容易
分析spydroid-ipcamera實現(xiàn):
- 該項目分別搭建了rstp server和一個本地的http web server傍药,我們這一篇文章主要分析CustomHttpServer和改動這個類
-
CustomHttpServer繼承TinyHttpServer類(簡便封裝的Server端磺平,基于org.apache.http框架)魂仍,注意此處拐辽,我們在使用的時候,往往會出現(xiàn)V4包版本沖突的問題擦酌,這是由于google在新版本的v4包不再使用這個框架的緣故俱诸,故此我們只要在app gradle中聲明
android { useLibrary 'org.apache.http.legacy' }
-
在TinyHttpServer中,方法addRequestHandler赊舶,學過java web的都知道睁搭,此處為解析請求內(nèi)容的結(jié)構(gòu)體
//在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的應(yīng)該都會证逻,不做簡述 } //.sdp請求: SDP會話描述協(xié)議:為會話通知碍岔、會話邀請和其它形式的多媒體會話初始化等目的提供了多媒體會話描述壁袄。 會話目錄用于協(xié)助多媒體會議的通告,并為會話參與者傳送相關(guān)設(shè)置信息锌唾。 SDP 即用于將這種信息傳輸?shù)浇邮斩恕? SDP 完全是一種會話描述格式――它不屬于傳輸協(xié)議 ――它只使用不同的適當?shù)膫鬏攨f(xié)議,包括會話通知協(xié)議 (SAP) 夺英、會話初始協(xié)議(SIP) 晌涕、實時流協(xié)議 (RTSP)、 MIME 擴展協(xié)議的電子郵件以及超文本傳輸協(xié)議 (HTTP)痛悯。SDP 的設(shè)計宗旨是通用性余黎, 它可以應(yīng)用于大范圍的網(wǎng)絡(luò)環(huán)境和應(yīng)用程序,而不僅僅局限于組播會話目錄
-
在DescriptionRequestHandler處理網(wǎng)絡(luò) request 和 response载萌, request我們在android中經(jīng)常寫惧财,現(xiàn)在讓我們來寫一次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. 啟動服務(wù)可缚,別忘了在清單文件中聲明 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中處理相應(yīng)的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(); } } };
-
接下來實現(xiàn)我們的Onvif Server:
首先選取我們要用的,TinyHttpServer是我們需要的斋枢,參照CustomHttpServer寫一個OnvifHttpServer,其中web訪問我們不做帘靡,可以去掉
-
自定義一個OnvifHttpServer集成TinyHttpServer,對照CustomHttpServer實現(xiàn)相應(yīng)方法瓤帚,Onvif Client請求端請求的方式是post,請求方式我們就假裝不知道描姚,我們定義Request pattern為所有格式即可,即: "/*"
@Override public void onCreate() { super.onCreate(); mDescriptionRequestHandler = new DescriptionRequestHandler(); mDescriptionOnvifHandler = new DescriptionOnvifHandler(); addRequestHandler("/*", mDescriptionOnvifHandler); addRequestHandler("/request.json*", new CustomRequestHandler()); }
-
實現(xiàn)基于Onvif的請求解析DescriptionRequestHandler戈次,返回的格式為:"application/soap+xml; charset=UTF-8"轩勘,接口有很多,要想實現(xiàn)標準工具播放怯邪,要實現(xiàn)很多很多的接口绊寻,下面簡單介紹幾個,感興趣的可以自己去抓包標準設(shè)備完成:
GetDeviceInformation:獲取設(shè)備信息
GetCapabilities :獲取設(shè)備性能
GetProfiles獲取設(shè)備權(quán)限
-
GetStreamUri 獲取設(shè)備流媒體服務(wù)地址和相應(yīng)信息
/** * 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")); //此處需要用到響應(yīng)的你想給請求設(shè)備的信息,我作為一個全局的bean類保存澄步,此處可以實現(xiàn)為序列化到本地或者sp保存都行 DevicesBackBean devicesBack = mApplication.getDevicesBack(); LogUtils.e(TAG, "DescriptionRequestHandler :" + devicesBack.toString()); String backS = null; //接下來就是返回請求了冰蘑,這里可以解析請求的包,即XML數(shù)據(jù)村缸,判斷相應(yīng)為什么請求祠肥,這個地方就體現(xiàn)了,模擬Onvif ipc //的尿性之處梯皿,請求接口相當繁多復(fù)雜仇箱,蛋疼的一匹,這樣實現(xiàn)終非正道东羹,如果要完成一個標識的Onvif IPC端還是要采取我第 //一篇文章的移植才是正軌剂桥,這里簡要實現(xiàn)幾個接口,作為參考属提,大家有問題可以私信我一起溝通 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")) {//需要鑒權(quán) 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>
-
返回端給出的代碼只需要把相應(yīng)的需要返回的信息替代到里面字符串即可
-
關(guān)于鑒權(quán)方面渊额,Onvif的鑒權(quán)有2中,WS-username token和Digest垒拢,可參考文章:Onvif協(xié)議及其在Android下的實現(xiàn)官方格式為
Digest=B64Encode(SHA1(B64ENCODE(Nonce)+Date+Password)) //nonce只是一個16位隨機數(shù)即可 //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); }
三、當當當當當求类!奔垦,完成上面的服務(wù)框架搭建,啟動服務(wù)
可通過標準工具檢查到我們的設(shè)備啦尸疆,并且還能查詢到響應(yīng)的信息
- 其中EP Address即在文章初始椿猎,udp返回中的getUrnUuid,為我根據(jù)android 特定機器碼生成的一個特定唯一的UUID