android版本的onvif及rtsp协议基本使用介绍
5G时代即将到来,万物联网都将成为现实。世面上的物联网产品越来越多,智能机器人、智能家居、智能货柜等等。很荣幸从学校一毕业就接触了这个行业,也很喜欢这个行业,是的开发调试变得没有那么枯燥。废话不多说,讲讲看今天要写的东西。
我在做的是一个智能货柜项目,基于动态图像识别技术,这个技术目前来看行业上面还是比较领先。当然我不也是搞算法那块,我没有那么牛逼,我负责的货柜上的主控系统。既然是图像识别,必然少不了的是摄像头,我们的识别算法在于云端,也选择的是网路摄像头,为了便宜选择的也是小厂牌的摄像头。目前世面上的网络摄像头,大都基于onvif协议实现,很不巧的是目前世面上大都是基于嵌入式c写的onvif代码。Andorid能找到的资源还是有限。然后自己只能蒙头网上找啊找,看啊看,终于摸到了一点门道,至少自己能够获取摄像头的截图了。这让我很是欣慰,兴奋的想记录一下。
其中onvif已经实现了截图、重启、修改ip、修改获取参数、固件升级,修改添加摄像头账号密码等功能
这个是我上传到github的onvif源码希望有帮助:
onvif协议实现源码
https://github.com/PetterJong/android-onvif
rtsp源码
https://github.com/PetterJong/rtsplib
Onvif协议基本介绍
ONVIF致力于通过全球性的开放接口标准来推进网络视频在安防市场的应用,这一接口标准将确保不同厂商生产的网络视频产品具有互通性。2008年11月,论坛正式发布了ONVIF第一版规范——ONVIF核心规范1.0。
既然我们要控制摄像头,必不可少的我们需要首先对onvif协议做些基础知识的了解,另外还需要掌握其鉴权方式,需要用到http鉴权和WS-UsernameToken临牌验证,这个很重要,当然这两种鉴权方式都已在代码中集成,完全有兴趣的同学可以去了解一下。强烈推荐使用令牌验证的方式,此方式减少一半的请求响应次数,http鉴权在我们获取设备截图的时候必须会使用到,也需要加强了解。关于这两种鉴权方式,可以参考我的文章onvif协议控制之鉴权方式。
注意:需要确保摄像头和我们的安卓设备的IP地址在同一网段下,否者将无法探测到该设备。
建议学习方式如下,首先需要下载一个onvif协议2.0的文档做参考,第二步需要下载onvif device manager(简称odm)软件做功能测试,查看摄像头支持那些功能,第三部下载Wireshark抓包工具,结合odm和抓包工具找到设置方法。第4步下载onvif device test tool工具,验证抓到的数据是否能够执行
所有的assets文件,包含鉴权信息及请求数据,重要必传参,建议先喽一眼:soap请求文件
1.搜索可用的设备
package com.wp.android_onvif.onvif;
import android.content.Context;
import android.util.Log;
import com.wp.android_onvif.onvifBean.Device;
import com.wp.android_onvif.util.XmlDecodeUtil;
import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
/**
* Description : 利用线程搜索局域网内设备
*/
public class FindDevicesThread extends Thread {
private static String tag = "OnvifSdk";
private byte[] sendData;
private boolean readResult = false;
private String ipAdress;
//回调借口
private FindDevicesListener listener;
/**
*
* @param context
* @param ipAdress ip地址(192.168.1.1)
* @param listener
*/
public FindDevicesThread(Context context, String ipAdress, FindDevicesListener listener) {
this.listener = listener;
this.ipAdress = ipAdress;
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 * 16];
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(ipAdress));
sendPacket.setPort(BROADCAST_PORT);
//发送
udpSocket.send(sendPacket);
//接受数据
receivePacket = new DatagramPacket(by, by.length);
long startTime = System.currentTimeMillis();
long endTime = System.currentTimeMillis();
while (endTime - startTime < 4 * 1000) {
udpSocket.receive(receivePacket);
String str = new String(receivePacket.getData(), 0, receivePacket.getLength());
Log.v(tag, str);
devices.add(XmlDecodeUtil.getDeviceInfo(str));
endTime = System.currentTimeMillis();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (udpSocket != null) {
udpSocket.close();
}
}
}
//回调结果
if (listener != null) {
listener.searchResult(devices);
}
}
/**
* Author : BlackHao
* Time : 2018/1/9 11:13
* Description : 搜索设备回调
*/
public interface FindDevicesListener {
void searchResult(ArrayList<Device> devices);
}
}
2.获取摄像头信息
在这里我们会拿到摄像头videoToken、profileToken和网络参数等信息,后面的请求需要用到这些参数
package com.wp.android_onvif.onvif;
import android.content.Context;
import com.wp.android_onvif.onvifBean.Device;
import com.wp.android_onvif.onvifBean.Digest;
import com.wp.android_onvif.onvifBean.MediaProfile;
import com.wp.android_onvif.util.Gsoap;
import com.wp.android_onvif.util.HttpUtil;
import com.wp.android_onvif.util.XmlDecodeUtil;
import java.io.IOException;
import java.io.InputStream;
/**
* 获取设备信息
*/
public class GetDeviceInfoThread extends Thread {
private static String tag = "OnvifSdk";
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 = OnvifUtils.getPostString("getCapabilities.xml", context, device, false);
String caps = HttpUtil.postRequest(device.getServiceUrl(), postString);
//解析返回的xml数据获取存在的url
XmlDecodeUtil.getCapabilitiesUrl(caps, device);
// getDeviceInformation 获取设备信息
String deviceInformation = OnvifUtils.getPostString("getDeviceInformation.xml", context, device, true);
String deviceInformationReturn = HttpUtil.postRequest(device.getServiceUrl(), deviceInformation);
XmlDecodeUtil.getDeviceInformation(deviceInformationReturn, device);
// 获取网络接口配置
String getNetworkInterface = OnvifUtils.getPostString("getNetworkInterface.xml", context, device, true);
String getNetworkInterfaceReturn = HttpUtil.postRequest(device.getServiceUrl(), getNetworkInterface);
XmlDecodeUtil.getNetworkInterface(getNetworkInterfaceReturn, device);
//getProfiles,需要鉴权
postString = OnvifUtils.getPostString("getProfiles.xml", context, device, true);
String profilesString = HttpUtil.postRequest(device.getMediaUrl(), postString);
//解析获取MediaProfile 集合
device.addProfiles(XmlDecodeUtil.getMediaProfiles(profilesString));
//通过token获取RTSP url
for (MediaProfile profile : device.getProfiles()) {
postString = OnvifUtils.getPostString("getStreamUri.xml", context, device, true, profile.getToken());
String profileString = HttpUtil.postRequest(device.getMediaUrl(), postString);
//解析获取mediaUrl
profile.setRtspUrl(XmlDecodeUtil.getStreamUri(profileString));
}
callBack.getDeviceInfoResult(true, device, "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,device, e.toString());
}
}
/**
* Author : BlackHao
* Time : 2018/1/11 14:24
* Description : 获取 device 信息回调
*/
public interface GetDeviceInfoCallBack {
void getDeviceInfoResult(boolean isSuccess, Device device, String errorMsg);
}
}
3.获取摄像头截图
package com.wp.android_onvif.onvif;
import android.content.Context;
import android.util.Log;
import com.wp.android_onvif.onvifBean.Device;
import com.wp.android_onvif.onvifBean.MediaProfile;
import com.wp.android_onvif.util.FileUtils;
import com.wp.android_onvif.util.HttpUtil;
import com.wp.android_onvif.util.LogClientUtils;
import com.wp.android_onvif.util.XmlDecodeUtil;
/**
* 获取摄像机截图
*/
public class GetSnapshotInfoThread extends Thread{
private static String tag = "OnvifSdk";
private Device device;
private Context context;
private GetSnapshotInfoThread.GetSnapshotInfoCallBack callBack;
private MediaProfile mediaProfile;
private String picRootPath;
private String picFileName;
// private WriteFileUtil util;
public GetSnapshotInfoThread(Device device, Context context, GetSnapshotInfoThread.GetSnapshotInfoCallBack callBack) {
this.device = device;
this.context = context;
this.callBack = callBack;
if(device.getProfiles() != null && device.getProfiles().size() > 0){
this.mediaProfile = device.getProfiles().iterator().next();
}
// util = new WriteFileUtil("onvif.txt");
}
public void setPath(String picRootPath, String picFileName){
this.picRootPath = picRootPath;
this.picFileName = picFileName;
}
@Override
public void run() {
super.run();
try {
//getProfiles,需要鉴权
String postString = OnvifUtils.getPostString("getSnapshotUri.xml", context, device,true,
mediaProfile == null?"000":mediaProfile.getToken());
String getSnapshotString = HttpUtil.postRequest(device.getMediaUrl(), postString);
Log.v(tag, getSnapshotString);
//解析获取MediaProfile 集合
String uri = XmlDecodeUtil.getSnapshotUri(getSnapshotString);
byte[] bytes = HttpUtil.getByteArray2(uri, device.getUserName(), device.getPsw());
String path = FileUtils.writeResoursToSDCard(picRootPath , picFileName, bytes);
callBack.getSnapshotInfoResult(true, path);
} catch (Exception e) {
e.printStackTrace();
callBack.getSnapshotInfoResult(false, e.toString());
}
}
public interface GetSnapshotInfoCallBack{
void getSnapshotInfoResult(boolean isSuccess, String errorMsg);
}
}
/**
* HA1 = MD5(<username>:<reaml>:<psd>)
* HA1 = MD5(<method>:<disgestUriPath>)
* Response = MD5(MD5(A1):<nonce>:<nc>:<conce>:<qop>:MD5(A2))
* 对用户名、认证域(realm)以及密码的合并值计算 MD5 哈希值,结果称为 HA1。
* 对HTTP方法以及URI的摘要的合并值计算 MD5 哈希值,例如,"GET" 和 "/dir/index.html",结果称为 HA2。
* 对 HA1、服务器密码随机数(nonce)、请求计数(nc)、客户端密码随机数(cnonce)、保护质量(qop)以及 HA2 的合并值计算 MD5 哈希值。结果即为客户端提供的 response 值。
* 因为服务器拥有与客户端同样的信息,因此服务器可以进行同样的计算,以验证客户端提交的 response 值的正确性。在上面给出的例子中,结果是如下计算的。 (MD5()表示用于计算 MD5 哈希值的函数;“\”表示接下一行;引号并不参与计算)
* 根据上面的算法所给出的示例,将在每步得出如下结果。
* HA1=MD5(admin:DS-2CD2310FD-I:hibox123) = 5a423defbb16f11b397b926915dfaa3b
* HA2 = MD5("GET:/onvif-http/snapshot?Profile_1") = 73d467d4c78c8f2e040b2943c9545432
* Response = MD5( "5a423defbb16f11b397b926915dfaa3b:4d6b5a444f5455774d7a6b364e575669597a67794d446b3d:00000001:cuYIxHJg3bt4gqYZwqndaAkuyUXtkE8b:auth:73d467d4c78c8f2e040b2943c9545432" )
* = 5920528fb9287d0ef90735acf122f695
* HEAD = Digest username="admin",realm="DS-2CD2310FD-I",nonce="4d6b5a444f5455774d7a6b364e575669597a67794d446b3d",uri="/onvif-http/snapshot?Profile_1",cnonce="cuYIxHJg3bt4gqYZwqndaAkuyUXtkE8b",nc=00000001,response="5920528fb9287d0ef90735acf122f695",qop="auth"
* @param url
* @return
*/
public static byte[] getByteArray2(String url, String user, String psd) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// .header("Authorization", "Client-ID " + UUID.randomUUID())
.url(url)
.get()
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful() ) {
if( response.code() == 200 && response.body() != null){
return response.body().bytes();
}
} else if(response.code() == 401){ // 未鉴权去鉴权
//WWW-Authenticate: Digest qop="auth", realm="DS-2CD2310FD-I", nonce="4d6a4931516a51304e554d364e445935595759785954553d", stale="TRUE"
//WWW-Authenticate: Basic realm="DS-2CD2310FD-I"
Headers h = response.headers();
List<String> auths = h.values("WWW-Authenticate");
Pattern qopPattern = Pattern.compile("qop=\"(.*?)\"");
Pattern realmPattern = Pattern.compile("realm=\"(.*?)\"");
Pattern noncePattern = Pattern.compile("nonce=\"(.*?)\"");
String qop = "";
String realm = "";
String nonce = "";
String method = request.method();
String host = response.request().url().host();
String disgestUriPath = url.split(host)[1];
for (String head: auths) {
Matcher qopMatcher = qopPattern.matcher(head);
while (qopMatcher.find()){
try{
qop = qopMatcher.group(1);
} catch (Exception e){
LogClientUtils.d(tag, e.getMessage());
}
}
Matcher realmMatcher = realmPattern.matcher(head);
while (realmMatcher.find()){
try{
realm = realmMatcher.group(1);
} catch (Exception e){
LogClientUtils.d(tag, e.getMessage());
}
}
Matcher nonceMatcher = noncePattern.matcher(head);
while (nonceMatcher.find()){
try{
nonce = nonceMatcher.group(1);
} catch (Exception e){
LogClientUtils.d(tag, e.getMessage());
}
}
}
return degistHttp(url, user, psd, method, disgestUriPath, nonce, realm, qop);
}
return null;
}
/**
* http鉴权
* @param url
* @param user
* @param psd
* @param method
* @param disgestUriPath
* @param nonce
* @param realm
* @param qop
* @return
* @throws IOException
*/
private static byte[] degistHttp(String url, String user, String psd, String method, String disgestUriPath, String nonce, String realm, String qop) throws IOException {
String nc = "00000001";
String cnonce = getNonce();
String ha1Data = getMd5Data(user, realm, psd);
String ha1 = MD5Util.MD5Encode(ha1Data);
String ha2Data = getMd5Data(method, disgestUriPath);
String ha2 = MD5Util.MD5Encode(ha2Data);
String ha3Data = getMd5Data(ha1, nonce, nc, cnonce, qop, ha2);
String responseData = MD5Util.MD5Encode(ha3Data);
String headFormat = "Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",cnonce=\"%s\",nc=%s,response=\"%s\",qop=\"%s\"" ;
String head = String.format(headFormat, user, realm, nonce, disgestUriPath, cnonce, nc, responseData, qop);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
// .header("Authorization", "Client-ID " + UUID.randomUUID())
.url(url)
.addHeader("Authorization", head)
.get()
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful() ) {
if( response.code() == 200 && response.body() != null){
return response.body().bytes();
}
}
return null;
}
private static String getMd5Data(String... params){
StringBuilder sb = new StringBuilder();
for (String param : params){
sb.append(param).append(":");
}
String data = sb.toString();
return data.substring(0, data.length() - 1);
}
/**
* 获取 Nonce
*
* @return Nonce
*/
private static String getNonce() {
//初始化随机数
Random r = new Random();
String text = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
String nonce = "";
for (int i = 0; i < 32; i++) {
int index = r.nextInt(text.length());
nonce = nonce + text.charAt(index);
}
return nonce;
}
4.其他功能不一一介绍,可以在github中,自行学习
rtsp的基本使用和介绍
rtsp-rtp是一个实时流协议,用来实时获取摄像头的视频流。
首先我们需要大致了解一下需要用到的协议
1.tcp协议 tcp协议是一个传输层的协议,具体不详细解释,自行百度。要说得一点就是,我们接下来的所有控制和操作都是基于这个协议的。详细介绍可以参考下这篇文章写的挺好的(https://blog.csdn.net/petershina/article/details/8189393 )
2.rtsp协议 rtsp协议,是一个控制协议,其中包含的常见操作方法有option、describe、setup、play、teardown、stop,通过这些方法我们可以开始或者停止获取视频流
3.sdp会话协议当我们通过rtsp协议去控制视频的时候,设备会给我们对应的回应,回应的数据用的就是sdp会话协议
4.RTSP Interleaved Frame这个我的理解是rtp协议的外部头信息,可以认为他是一种格式,独立于rtp协议。它占4个字节,描述了接下来数据的格式和大小,请看下面的图。
5.rtp-rtcp协议这个是视频流协议,我用的rtp协议,通过解码可以拿到原始的数据流,这个协议的数据紧跟在RTSP Interleaved Frame格式后面。rtcp适用于网络直播,会根据网络状态,动态调整帧率和码率,从而达到高效。
6.h264协议 这个是一个视频协议,跟mp4协议是一个级别,播放器可以直接播放了。
我的rtsp源码不包含demo程序,初始化之后调用play就可以在回调接口里拿到视频流,最后输出的为h264的视频流。如果对mp4协议足够了解,你也可以讲rtp流转换成mp4流。
总结
rtsp-tcp/rtcp是一个实时流协议,只能用来获取实时流。
onvif 媒体配置是通过SOAP/HTTP协议完成的,SOAP是简单对象访问协议,可以粗俗的看成是一种xml协议。简单来说rtsp获取实时流,通过http请求,soap数对象封装来完成与onvif协议摄像头参数请求。
我们可以通过onvif的标准协议拿到设备的rtsp的uri和修改对应的摄像头参数,onvif协议是基于http的网络请求。需要注意的是很多摄像头本身可能本身并没有100%的实现onvif协议,这意味着这些设备的部分功能我们不能通过http onvif协议去修改或者操作该摄像头,我们可以借助odm软件工具去检查是否支持改功能,不支持的功能是灰色的无法选中。大部分摄像头厂商都会有自己的私有sdk,当通过公有的onvif协议无法获取到该摄像头参数时,不妨考虑联系下厂商给你提供帮助。
这里需要谢谢Black_hao :https://blog.csdn.net/a512337862/article/details/79281648