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;
}
}