1. 程式人生 > >【Android音視訊】Android Onvif-IPC開發(一)——在Android端搭建伺服器模擬Onvif-IP-Camera

【Android音視訊】Android Onvif-IPC開發(一)——在Android端搭建伺服器模擬Onvif-IP-Camera

Android端實現Onvif IPC開發:

本篇內容簡介:

本篇是上一文章移植失敗採取的第二方案,通過在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實現:

  1. 該專案分別搭建了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訪問我們不做,可以去掉

  1. 自定義一個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());
      }
    
  2. 實現基於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);
                          }
                 }
        }
      
  3. 下面給一個我抓包的請求:

    • 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>
      
  4. 返回端給出的程式碼只需要把相應的需要返回的資訊替代到裡面字串即可

  5. 關於鑑權方面,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_1.png onvif_2.png

接下來我們要搭建RTSP伺服器,即可以在標準工具中進行播放,請檢視我的下一篇文章:《Android端實現Onvif IPC開發(三)——在Android端搭建RTSP伺服器》