1. 程式人生 > >Android Onvif 搜尋IPC裝置以及獲取IPC裝置資訊

Android Onvif 搜尋IPC裝置以及獲取IPC裝置資訊

最近,在接觸onvif協議在Android端的實現。抓了無數的包,踩了無數的坑之後,利用取巧的方式也終於實現部分的功能,主要是搜尋IPC裝置,獲取IPC裝置的一些資訊:rtsp地址,音視訊編解碼資訊,雲臺資訊等。關於onvif協議請自行百度。

思路

這裡主要是將一些需要傳送的關鍵指令以檔案的形式儲存在assets中,需要時直接從assets中讀取指定的檔案,然後將一些關鍵的字元填充進去傳送給伺服器即可。專案的流程如下:
1.傳送廣播包搜尋區域網內的IPC。
2.通過IPC裝置的serviceUrl獲取IPC的能力集。
3.獲取IPC的Profiles
4.獲取IPC每個Profile的rtsp

程式碼實現

IPC裝置搜尋

IPC裝置搜尋主要是通過UDP廣播包來發送Probe請求,IPC裝置收到後會返回自己的裝置部分資訊。
搜尋程式碼如下:

/**
 * Author : BlackHao
 * Time : 2018/1/8 14:38
 * Description : 利用執行緒搜尋區域網內裝置
 */

public class FindDevicesThread extends Thread {

    private byte[] sendData;
    private boolean readResult = false, receiveTag = true
; //回撥藉口 private FindDevicesListener listener; public FindDevicesThread(Context context, FindDevicesListener listener) { this.listener = listener; InputStream fis = null; try { //從assets讀取檔案 fis = context.getAssets().open("probe.xml"); sendData = new
byte[fis.available()]; readResult = fis.read(sendData) > 0; } catch (Exception e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Override public void run() { super.run(); DatagramSocket udpSocket = null; DatagramPacket receivePacket; DatagramPacket sendPacket; //裝置列表集合 ArrayList<Device> devices = new ArrayList<>(); byte[] by = new byte[1024 * 3]; if (readResult) { try { //埠號 int BROADCAST_PORT = 3702; //初始化 udpSocket = new DatagramSocket(BROADCAST_PORT); udpSocket.setSoTimeout(4 * 1000); udpSocket.setBroadcast(true); //DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length); sendPacket.setAddress(InetAddress.getByName("239.255.255.250")); sendPacket.setPort(BROADCAST_PORT); //傳送 udpSocket.send(sendPacket); //接受資料 receivePacket = new DatagramPacket(by, by.length); while (receiveTag) { udpSocket.receive(receivePacket); String str = new String(receivePacket.getData(), 0, receivePacket.getLength()); devices.add(XmlDecodeUtil.getDeviceInfo(str)); } } catch (Exception e) { e.printStackTrace(); } finally { if (udpSocket != null) { udpSocket.close(); } } } receiveTag = false; //回撥結果 if (listener != null) { listener.searchResult(devices); } } /** * Author : BlackHao * Time : 2018/1/9 11:13 * Description : 搜尋裝置回撥 */ public interface FindDevicesListener { void searchResult(ArrayList<Device> devices); } }

獲取到IPC裝置返回的資料,截圖如下:
這裡寫圖片描述
在對資料進行xml解析,得到IPC的地址,我這裡是新建了一個Device類來儲存裝置的資訊,具體的Device類,可以參考原始碼。
到這裡基本上IPC搜尋就算完成了。

獲取IPC裝置的資訊

獲取IPC資訊,主要是通過HTTP將資料POST到指定的URL,然後在解析返回的xml資料,獲取需要的資訊即可。關鍵程式碼如下:

/**
 * Author : BlackHao
 * Time : 2018/1/11 14:20
 * Description : 獲取 device 相關資訊
 */

public class GetDeviceInfoThread extends Thread {

    private Device device;
    private Context context;
    private GetDeviceInfoCallBack callBack;

    private WriteFileUtil util;

    public GetDeviceInfoThread(Device device, Context context, GetDeviceInfoCallBack callBack) {
        this.device = device;
        this.context = context;
        this.callBack = callBack;
        util = new WriteFileUtil("onvif.txt");
    }

    @Override
    public void run() {
        super.run();
        try {
            //getCapabilities,不需要鑑權
            String postString = getPostString("getCapabilities.xml", false);
            String caps = HttpUtil.postRequest(device.getServiceUrl(), postString);
            //解析返回的xml資料獲取存在的url
            XmlDecodeUtil.getCapabilitiesUrl(caps, device);
            //getProfiles,需要鑑權
            postString = getPostString("getProfiles.xml", true);
            String profilesString = HttpUtil.postRequest(device.getMediaUrl(), postString);
            //解析獲取MediaProfile 集合
            device.addProfiles(XmlDecodeUtil.getMediaProfiles(profilesString));
            //通過token獲取RTSP url
            for (MediaProfile profile : device.getProfiles()) {
                postString = getPostString("getStreamUri.xml", true, profile.getToken());
                String profileString = HttpUtil.postRequest(device.getMediaUrl(), postString);
                //解析獲取mediaUrl
                profile.setRtspUrl(XmlDecodeUtil.getStreamUri(profileString));
            }
            callBack.getDeviceInfoResult(true, "NO_ERROR");

//            postString = getPostString("getConfigOptions.xml", true);
//            caps = HttpUtil.postRequest(device.getPtzUrl(), postString);
//            util.writeData(caps.getBytes());

            util.finishWrite();
        } catch (Exception e) {
            e.printStackTrace();
            callBack.getDeviceInfoResult(false, e.toString());
        }
    }

    /**
     * 通過使用者名稱/密碼/assets 檔案獲取對應需要傳送的String
     *
     * @param fileName   assets檔名
     * @param needDigest 是否需要鑑權
     * @return 需要傳送的 string
     */
    private String getPostString(String fileName, boolean needDigest, String... params) throws IOException {
        //讀取檔案內容
        String postString = "";
        InputStream is = context.getAssets().open(fileName);
        byte[] postData = new byte[is.available()];
        if (is.read(postData) > 0) {
            postString = new String(postData, "utf-8");
        }
        //獲取digest
        Digest digest = Gsoap.getDigest(device.getUserName(), device.getPsw());
        //需要digest
        if (needDigest && digest != null) {
            if (params.length > 0) {
                postString = String.format(postString, digest.getUserName(),
                        digest.getEncodePsw(), digest.getNonce(), digest.getCreatedTime(), params[0]);
            } else {
                postString = String.format(postString, digest.getUserName(),
                        digest.getEncodePsw(), digest.getNonce(), digest.getCreatedTime());
            }

        }
        return postString;
    }

    /**
     * Author : BlackHao
     * Time : 2018/1/11 14:24
     * Description : 獲取 device 資訊回撥
     */
    public interface GetDeviceInfoCallBack {
        void getDeviceInfoResult(boolean isSuccess, String errorMsg);
    }
}

主要流程
1.通過ServiceUrl獲取IPC的Capabilities,主要是MediaURL(用於獲取音視訊相關資訊)和PtzURL(用於獲取雲臺相關資訊),其他的根據自己需要決定。PS:部分返回資訊截圖:
這裡寫圖片描述

2.通過MediaUrl獲取IPC的Profiles,主要是音視訊的編碼資訊、ProfileToken(主要用於獲取該Profile的rtspURL)、PTZ配置資訊等。PS:需要鑑權一個IPC可能會有多個Profile,部分返回資訊截圖:
這裡寫圖片描述

3..通過MediaUrl以及ProfileToken,獲取對應的rtspURL。PS:需要鑑權,部分返回資訊截圖:
這裡寫圖片描述

解析返回的xml資料沒有太多可說的,就是通過XmlPullParser來進行解析。

這裡需要重點介紹的是onvif鑑權:所謂的WS_UsernameToken加密,就是將 使用者名稱,密碼,Nonce,Created都包含在了header裡面。如果將#passwordDigest換成#passwordText的話,密碼就是明文的,當然onvif說了,密碼是Digest。
我們知道了使用者名稱密碼,那如何驗證呢?文件裡面提到了獲取Digest的公式:
Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )

鑑權的Header格式如下:

 <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”替換,需要用的時候直接替換即可。獲取Digest 的程式碼如下:

Digest

/**
 * Author : BlackHao
 * Time : 2018/1/10 14:08
 * Description : onvif Digest
 */

public class Digest {
    private String userName;
    private String nonce;
    private String encodePsw;
    private String createdTime;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNonce() {
        return nonce;
    }

    public void setNonce(String nonce) {
        this.nonce = nonce;
    }

    public String getEncodePsw() {
        return encodePsw;
    }

    public void setEncodePsw(String encodePsw) {
        this.encodePsw = encodePsw;
    }

    public String getCreatedTime() {
        return createdTime;
    }

    public void setCreatedTime(String createdTime) {
        this.createdTime = createdTime;
    }

    @Override
    public String toString() {
        return "Digest{" +
                "userName='" + userName + '\'' +
                ", nonce='" + nonce + '\'' +
                ", encodePsw='" + encodePsw + '\'' +
                ", createdTime='" + createdTime + '\'' +
                '}';
    }
}

Gsoap(用於生成Digset)

/**
 * Author : BlackHao
 * Time : 2018/1/10 09:39
 * Description : 獲取 onvif Digest
 */

public class Gsoap {

    /**
     * Digest = B64ENCODE( SHA1( B64DECODE( Nonce ) + Date + Password ) )
     * 生成 Digest
     */
    public static Digest getDigest(String userName, String psw) {
        Digest digest = new Digest();
        String nonce = getNonce();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'",
                Locale.CHINA);
        String time = df.format(new Date());
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            // 從官方文件可以知道我們nonce還需要用Base64解碼一次
            byte[] b1 = Base64.decode(nonce.getBytes(), Base64.DEFAULT);
            // 生成字元位元組流
            byte[] b2 = time.getBytes(); // "2018-01-10T11:00:00Z";
            byte[] b3 = psw.getBytes();
            // 根據我們傳得值的長度生成流的長度
            byte[] b4;
            // 利用sha-1加密字元
            md.update(b1, 0, b1.length);
            md.update(b2, 0, b2.length);
            md.update(b3, 0, b3.length);
            // 生成sha-1加密後的流
            b4 = md.digest();
            // 生成最終的加密字串
            String result = new String(Base64.encode(b4, Base64.DEFAULT));
//            Log.e("Gsoap", result);
            digest.setNonce(nonce);
            digest.setCreatedTime(time);
            digest.setUserName(userName);
            digest.setEncodePsw(result);
            return digest;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 獲取 Nonce
     *
     * @return Nonce
     */
    private static String getNonce() {
        //初始化隨機數
        Random r = new Random();
        String text = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        String nonce = "";
        for (int i = 0; i < text.length(); i++) {
            int index = r.nextInt(text.length());
            nonce = nonce + text.charAt(index);
        }
        return nonce;
    }
}

結語