【Android音视频】Onvif-IPC开发(一)——gSoap-onvif移植到Android》
【Android音视频】Onvif-IPC开发(二)——JAVA版本onvif服务器构建Onvif-IP-Camera
【Android音视频】Onvif IPC开发(三)——YUV格式深入浅出
[【Android音视频】Onvif IPC开发(四)——Onvif移植Android架构与补全方案(更新中…) ]
由于搞这个项目时,参考并阅读了许多资料,可能存在相似却未声明的借鉴之处,请联系我修改或声明
本篇是上一文章移植失败采取的第二方案,通过在android搭建service,模拟成一个onvif协议对接的IPC端,在这之前,首先需要明白,onvif设备对接的流程或者说方式,接下来的文章内容也是基于下面一条流程去实现。
IPC设备基于Onvif被发现,首先要明白 WS-Discovery: 动态的探测可用服务并调用之
注意点:
此处为demo,在项目中最好将这个广播添加到Android service中进行
android需要使用组播MulticastSocket实现udp搜索功能,此处需要权限:
// 允许应用程序访问WIFI网卡的网络信息
// 允许应用程序访问有关的网络信息
//android 组播功能权限
回传的值可以通过在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("\r\n");
sb.append("\r\n");
sb.append(" \r\n");
sb.append(" urn:uuid" + urn_uuid + " \r\n");
sb.append(" " + uuid_req + " \r\n");
sb.append(" \r\n");
sb.append(" http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous \r\n");
sb.append(" \r\n");
sb.append(" http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous \r\n");
sb.append(" http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" urn:uuid:" + urn_uuid + " \r\n");
sb.append(" \r\n");
sb.append(" dn:NetworkVideoTransmitter \r\n");
sb.append(" 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 \r\n");
sb.append(" http://" + address_local + ":8080/onvif/device_service \r\n");
sb.append(" 10 \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" \r\n");
sb.append(" ");
return sb.toString();
}
请求的Probe抓包:
uuid:d9305f63-5027-4edb-b1f1-58c463b5419a
urn:schemas-xmlsoap-org:ws:2005:04:discovery
http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe
tds:Device
其中OnvifXmlResolver为xml解析,感兴趣的可以看我的文章: SAX解析XML文件,通过解析相应节点去获取需要的内容,以下都会有用到,这个代码会在下一篇文章总给出
完成以上内容既可以被搜索到了,干净用工具测试一下,发现OK!
服务端这边主要由开源框架spydroid-ipcamera改动得来,项目地址在文章头部给出,读者可以下载阅读,其实改起来很简单,下面先对该项目简单分析:
在阅读下面分析时,最好在下载该项目,进行简单阅读或对比阅读,这样会更明了,我本人在写博客时更提倡读者自己动手,而不是直接给个现成的demo
这个工程只要有大体的实现思维,改动起来其实很容易
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 * or *
* @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 params = URLEncodedUtils.parse(URI.create(uri),"UTF-8");
uri = "";
if (params.size()>0) {
for (Iterator 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();
}
}
};
首先选取我们要用的,TinyHttpServer是我们需要的,参照CustomHttpServer写一个OnvifHttpServer,其中web访问我们不做,可以去掉
自定义一个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());
}
实现基于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);
}
}
}
下面给一个我抓包的请求:
getProfiles和getProfilesBack:
%s
%s
%s
%s
%s
Profile1
VideoSourceConfig 1 VideoSource_1
VideoEncoder_1 1 %s
%s %s
44.0
%s 1 %s
100 Baseline
IPv4 0.0.0.0
0
0
false
PT30S
PTZ
1
PTZNODETOKEN
http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace
http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace
http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace
http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace
http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace
http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace
PT1S
http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace
-INF INF
-INF INF
http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace
-INF
INF
返回端给出的代码只需要把相应的需要返回的信息替代到里面字符串即可
关于鉴权方面,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);
}
可通过标准工具检查到我们的设备啦,并且还能查询到响应的信息