2019.12.09 更新(重要!!!)
一,此博文及对应代码写于2018年初,基于海康SDK V5.3.3.2版本(当时最新版本),只适用于2019年前海康监控设备;(海康监控产品更新换代,旧版SDK不再适用于新产品。故老款设备监控视频可正常查看,新款无法查看)
二,近期已重新开发此功能,已兼容新/旧款设备,支持单路/多路实时监控,以及单路历史监控视频回放,且采用不同方案实现。详情如下:
部分效果图如下:
由于某些原因不便开源,有问题的朋友可私聊交流;
本篇文章讲述的是基于海康SDK播放监控视频,包含以下几个方面:
github地址:https://github.com/fangxiaopeng/fxp-plugin-video,包含完整代码及详细注释,欢迎star or fork
最近项目上有海康监控视频播放需求,包括Android 和 IOS两个版本。今天讲讲Android上的实现过程,后面有空了再讲讲IOS。
思路:基于海康视频SDK,使用AsyncTask登录设备后,获取实时流音视频数据,解码显示到SurfaceView。通过startActivityForResult的resultCode设置回调。
以下为工程文件目录结构及效果截图:
下面分阶段讲监控视频播放过程及需要用到的API。
1,登录设备
调用以下方法登录设备:
public int NET_DVR_Login_V30(String sDvrIp, int iDvrPort, ava.lang.String sUserName, String sPassword, NET_DVR_DEVICEINFO_V30 DeviceInfo)
参数说明:
[in] sDvrIp 设备 IP 地址或静态域名
[in] iDvrPort 设备端口号
[in] sUserName 登录的用户名
[in] sPassword 用户密码
[out] DeviceInfo 设备信息
返回值:
-1表示失败,其他值表示返回的用户ID值。该用户ID具有唯一性,后续对设备的操作都需要通过此ID实现。
这里需要讲下用于获取设备信息的NET_DVR_Login_V30类。
新建NET_DVR_Login_V30类对象,作为登录设备的一个参数传入,登录成功后即可返回设备详细信息,包含以下信息:
sSerialNumber 设备序列号
byAlarmInPortNum 报警输入个数
byAlarmOutPortNum 报警输出个数
byDiskNum 硬盘个数
byDVRType 设备类型
byChanNum 设备模拟通道个数
byStartChan 模拟通道起始通道号
byAudioChanNum 设备语音通道数
byIPChanNum 设备最大数字通道个数,低 8 位
byZeroChanNum 零通道个数
wDevType 设备类型
byStartDChan 起始数字通道号
byHighDChanNum 数字通道个数,高 8 位
2,获取实时流音视频数据
登录设备成功后,调用NET_DVR_RealPlay_V40
方法获取实时流音视频数据。
public int NET_DVR_RealPlay_V40(int lUserID, NET_DVR_PREVIEWINFO previewInfo, RealPlayCallBack CallBack)
参数说明:
[in] lUserID NET_DVR_Login_V30 的返回值
[in] previewInfo 预览参数,包括码流类型、取流协议、通道号等
[in] CallBack 码流数据回调函数
返回值:
-1 表示失败,其他值作为 NET_DVR_StopRealPlay 等函数的句柄参数。
这里需要重点讲下以下两个类:
(1)NET_DVR_PREVIEWINFO:用于设置预览参数。
可设置项如下:
lChannel 通道号,目前设备模拟通道号从 1 开始,数字通道的起始通道号一般从 33 开始,具体取值在登录接口返回
dwStreamType 码流类型:0-主码流,1-子码流,2-码流 3,3-虚拟码流,以此类推 dwLinkMode 连接方式:0- TCP 方式,1- UDP 方式,2- 多播方式,3- RTP 方式,4-RTP/RTSP,5-RSTP/HTTP
bBlocked 0- 非阻塞取流,1- 阻塞取流
bPassbackRecord 0-不启用录像回传,1-启用录像回传。ANR 断网补录功能,客户端和设备之间网络异常恢复之后自动将前端数据同步过来,需要设备支持。
byPreviewMode 预览模式:0- 正常预览,1- 延迟预览
byProtoType 应用层取流协议:0- 私有协议,1- RTSP 协议
hHwnd 播放窗口的句柄,为 NULL 表示不解码显示
(2)RealPlayCallback:用于获取实时音视频数据。实现的以下接口
public interface RealPlayCallBack {
public void fRealDataCallBack(int iRealHandle, int iDataType, byte[] pDataBuffer, int iDataSize);
}
参数说明:
[out] iRealHandle 当前的预览句柄
[out] iDataType 数据类型
[out] pDataBuffer 存放数据的缓冲区指针
[out] iDataSize 缓冲区大小
3,解码播放实时流音视频
调用NET_DVR_RealPlay_V40 方法获取实时流音视频数据成功后,通过播放库进行解码显示。
实现RealPlayCallback接口的fRealDataCallback方法,拿到实时流音视频数据,调用以下方法解码播放。
private void processData(int iPlayViewNo, int iDataType, byte[] pDataBuffer, int iDataSize, int iStreamMode) {
if (HCNetSDK.NET_DVR_SYSHEAD == iDataType) {
if (m_iPort >= 0) {
return;
}
m_iPort = Player.getInstance().getPort();
if (m_iPort == -1) {
Log.e(TAG, "getPort is failed with: " + Player.getInstance().getLastError(m_iPort));
return;
}
Log.i(TAG, "getPort succ with: " + m_iPort);
if (iDataSize > 0) {
if (!Player.getInstance().setStreamOpenMode(m_iPort, iStreamMode)) //set stream mode
{
Log.e(TAG, "setStreamOpenMode failed");
return;
}
if (!Player.getInstance().openStream(m_iPort, pDataBuffer, iDataSize, 2 * 1024 * 1024)) //open stream
{
Log.e(TAG, "openStream failed");
return;
}
while (!m_bSurfaceCreated) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Log.i(TAG, "wait 100 for surface, handle:" + iPlayViewNo);
}
if (!Player.getInstance().play(m_iPort, getHolder())) {
Log.e(TAG, "play failed,error:" + Player.getInstance().getLastError(m_iPort));
return;
}
if (!Player.getInstance().playSound(m_iPort)) {
Log.e(TAG, "playSound failed with error code:" + Player.getInstance().getLastError(m_iPort));
return;
}
}
} else {
if (!Player.getInstance().inputData(m_iPort, pDataBuffer, iDataSize)) {
Log.e(TAG, "inputData failed with: " + Player.getInstance().getLastError(m_iPort));
}
}
}
4,停止获取实时流音视频数据
调用以下方法停止获取实时流:
public boolean NET_DVR_StopRealPlay(int iRealHandle)
参数说明:
[in] iRealHandle 预览句柄,NET_DVR_RealPlay_V40 的返回值
返回值:
TRUE表示成功,FALSE表示失败。
5,停止本地播放
停止获取实时流音视频数据后,还需要手动关闭本地解码播放。可调用以下方法:
private void stopPlayer() {
Player.getInstance().stopSound();
if (!Player.getInstance().stop(this.m_iPort)) {
Log.e(TAG, "stop is failed!");
return;
}
if (!Player.getInstance().closeStream(this.m_iPort)) {
Log.e(TAG, "closeStream is failed!");
return;
}
if (Player.getInstance().freePort(this.m_iPort)) {
this.m_iPort = -1;
return;
}
Log.e(TAG, "freePort is failed!" + this.m_iPort);
}
6,注销登录
单个设备可同时连接的用户数量是有限制的,达到限制后其他用户无法再登录(登录返回错误码5)。所以我们不再预览时,应该注销登录。调用以下方法即可:
public boolean NET_DVR_Logout_V30 (int lUserID)
参数说明:
[in]lUserID 用户 ID 号,NET_DVR_Login_V30 的返回值
返回值:
TRUE 表示成功,FALSE表示失败。
至此,海康摄像头监控视频实时预览过程算是讲完了。下面讲部分具体实现:
1,项目架构
为了更好的支持后续功能拓展,也为了降低类的复杂度,提升代码可维护性,尽可能的秉持了单一职责原则和接口隔离原则。以下为部分类说明:
LoginAsyncTask.java 异步任务类登录设备
AsyncTaskExecuteListener.java 异步任务类执行结果监听接口
PlaySurfaceView.java 自定义SurfaceView播放视频
MonitorVedioActivity.java 界面交互Activity
MethodUtils.java 工具类
VideoInfo.java 设备实体类
2,登录设备
登录设备,然后根据登录返回信息获取实时流,这里用异步任务类再合适不过了。我是写了一个异步任务类配合接口完成设备
登录及登录结果获取功能,具体实现可查看github代码。
3,自动适配单路/多路视频,自定义播放列数
/**
* 开始播放实时监控视频
*
* @param chanNum 通道数目
* @param columnNum 展示列数
*/
private void startPreview(int chanNum, int columnNum) {
playView = new PlaySurfaceView[chanNum];
// 建立frameLayout容纳所有通道视频画面
FrameLayout videoLayout = new FrameLayout(this);
for (int i = 0; i < chanNum; i++) {
if (playView[i] == null) {
// 第i通道SurfaceView
playView[i] = new PlaySurfaceView(this);
// 设置第i通道监控画面尺寸,单路时全屏播放,多路时分列4:3播放
playView[i].setViewSize(metric.widthPixels / columnNum, columnNum == 1 ? metric.heightPixels : 3 * metric.widthPixels / (4 * columnNum));
// 设置第i通道监控画面布局参数
FrameLayout.LayoutParams videoItemParams = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
videoItemParams.topMargin = i / columnNum * playView[i].getCurHeight();
videoItemParams.leftMargin = i % columnNum * playView[i].getCurWidth();
videoItemParams.gravity = Gravity.TOP | Gravity.LEFT;
// 将第i通道SurfaceView添加到FrameLayout
videoLayout.addView(playView[i], videoItemParams);
}
// 播放第i通道视频
playView[i].startPreview(iLogId, i + iStartChan);
}
// 设置scrollView布局参数
FrameLayout.LayoutParams scrollViewParams = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
ScrollView scrollView = new ScrollView(this);
// 将含有多通道视频画面的frameLayout添加到scrollView
scrollView.addView(videoLayout);
// 动态添加scrollView布局
addContentView(scrollView, scrollViewParams);
// 获取起始通道监控视频播放状态码
iPlayId = playView[0].m_iPreviewHandle;
}
4,获取实时流音视频数据
public void startPreview(int iLogId, int chanNum) {
RealPlayCallBack realPlayCallBack = getRealPlayerCbf();
if (realPlayCallBack == null) {
Log.e(TAG, "fRealDataCallBack object is failed!");
return;
}
Log.i(TAG, "preview channel:" + chanNum);
NET_DVR_PREVIEWINFO netDVRPreviewInfo = new NET_DVR_PREVIEWINFO();
// 通道号,模拟通道号从1开始,数字通道号从33开始,具体取值在登录接口返回
netDVRPreviewInfo.lChannel = chanNum;
// 码流类型
netDVRPreviewInfo.dwStreamType = 1;
// 连接方式,0-TCP方式,1-UDP方式,2-多播方式,3-RTP方式,4-RTP/RTSP,5-RSTP/HTTP
// previewInfo.dwLinkMode = 5;
// 0-非阻塞取流,1-阻塞取流
netDVRPreviewInfo.bBlocked = 1;
// 实时预览,返回值-1表示失败
this.m_iPreviewHandle = HCNetSDK.getInstance().NET_DVR_RealPlay_V40(iLogId, netDVRPreviewInfo, realPlayCallBack);
if (m_iPreviewHandle < 0) {
Log.e(TAG, "NET_DVR_RealPlay is failed!Err:" + HCNetSDK.getInstance().NET_DVR_GetLastError());
}
}
5,解码播放实时流音视频
见前面过程讲解部分,或github代码,不再赘述。
6,SurfaceView播放视频
SurfaceView是View的子类,功能非常强大。它允许在其上添加层,允许其他线程更新视图对象,使用了双缓冲机制,非常适
合我们的需求。创建SurfaceView的时候需要实现SurfaceHolder.Callback接口,它可以用来监听SurfaceView的状态,比如:SurfaceView
的改变 、SurfaceView的创建 、SurfaceView 销毁等,我们可以在相应的方法中做一些操作。
这里我实现了SurfaceHolder.Callback接口,并重写了以下几个方法:
@Override
public void surfaceCreated(SurfaceHolder paramSurfaceHolder) {
videoSurfaceview.getHolder().setFormat(-3);
if ((-1 != iPort) && (paramSurfaceHolder.getSurface().isValid()))
Player.getInstance().setVideoWindow(iPort, 0, paramSurfaceHolder);
}
@Override
public void surfaceChanged(SurfaceHolder paramSurfaceHolder, int paramInt1, int paramInt2, int paramInt3) {
}
@Override
public void surfaceDestroyed(SurfaceHolder paramSurfaceHolder) {
if ((-1 != iPort) && (paramSurfaceHolder.getSurface().isValid()))
Player.getInstance().setVideoWindow(iPort, 0, null);
}
注意:必须实现SurfaceHolder.Callback接口并重写以上方法,不然会出现诸如锁屏再进入或切换到后台再进入后黑屏问题。
7,获取错误信息
(1)返回最后操作的错误码NET_DVR_GetLastError
// 调用 NET_DVR_GetLastError 获取错误码,通过错误码判断出错原因
int errorCode = HCNetSDK.getInstance().NET_DVR_GetLastError();
错误码详见《设备网络编程指南(Android)》第4章。
至此,Android平台的海康视频播放功能开发完成了。下面讲讲封装为Cordova插件。
我以前写过几篇封装Cordova插件的博文,业务需求虽不同,但方法是一样的,所以这里就不再重复写了。海康视频播放插件已上传到github,所有代码均有详细注释,github地址:https://github.com/fangxiaopeng/fxp-plugin-video。如果大家对Cordova插件开发有兴趣的话,可以看看我的另外几篇博文:
Cordova插件开发(1)-Android插件开发详解
Cordova插件开发(2)-Android插件安装包制作详解
Cordova插件开发(3)-将Cordova插件发布到npm
这里需要讲的是将视频播放结果设置到js回调。
思路:以startActivityForResult的方式启动视频播放Avtivity,在视频播放Activity退出时设置resultCode并传递信息,
在onActivityResult回调中获取对应Activity传递的信息,通过callbackContext设置js回调。
下面是具体实现:
(1)在继承于CordovaPlugin的类中,以startActivityForResult的方式启动视频播放Avtivity
private void toMonitorVideoActivity(VideoInfo videoInfo) {
Log.e(TAG, "toMonitorVideoActivity");
Intent intent = new Intent(this.cordova.getActivity(), MonitorVedioActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable("videoInfo", videoInfo);
intent.putExtras(bundle);
if (this.cordova != null) {
this.cordova.startActivityForResult((CordovaPlugin) this, intent, REQUEST_MonitorVideo);
}
}
(2)在视频播放Activity退出时设置resultCode并传递信息
/**
* 退出Activity,设置返回值
*
* @param activity 待退出activity
* @param resultCode 返回码
* @param msg 返回信息
*/
public void quitActivity(Activity activity,int resultCode, String msg){
Intent intent = new Intent();
intent.putExtra("result",msg);
activity.setResult(resultCode,intent);
activity.finish();
}
(3)在onActivityResult回调中获取对应Activity传递的信息
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
switch (requestCode){
case REQUEST_HCVideo:
Log.i(TAG,"back from HCVideoActivity");
break;
case REQUEST_MonitorVideo:
Log.i(TAG,"back from MonitorVedioActivity");
break;
}
setCallBack(resultCode,intent);
}
(4)通过callbackContext设置js回调
private void setCallBack(int resultCode, Intent intent){
if (this.callbackContext != null){
String resultMsg = intent.getExtras().getString("result");
Log.i(TAG,"resultCode:" + resultCode);
Log.i(TAG,"resultMsg:" + resultMsg);
switch (resultCode){
case RESULT_NORMAL:
Log.i(TAG,"RESULT_NORMAL");
callbackContext.success("success");
break;
case RESULT_ERROR:
Log.e(TAG,"RESULT_ERROR");
callbackContext.error(resultMsg);
break;
}
}
}
OK。