海康摄像头监控视频播放详解

2019.12.09 更新(重要!!!)

一,此博文及对应代码写于2018年初,基于海康SDK V5.3.3.2版本(当时最新版本),只适用于2019年前海康监控设备;(海康监控产品更新换代,旧版SDK不再适用于新产品。故老款设备监控视频可正常查看,新款无法查看)

二,近期已重新开发此功能,已兼容新/旧款设备,支持单路/多路实时监控,以及单路历史监控视频回放,且采用不同方案实现。详情如下:

  1. 单路视频预览(使用xml布局SurfaceView控件)
  2. 单路视频预览(动态new View 添加到 ViewGroup)
  3. 多路视频预览(动态new View 添加到 ViewGroup)
  4. 多路视频预览(使用RecyclerView)
  5. 单路视频回放(选择时间段)
  6. 多路视频预览集成示例-在指定ViewGroup显示(动态new View 添加到 ViewGroup)

部分效果图如下:

 

由于某些原因不便开源,有问题的朋友可私聊交流;

 


 本篇文章讲述的是基于海康SDK播放监控视频,包含以下几个方面:

  • 播放海康视频用到的API详解;
  • 播放海康视频实践过程详解;
  • 播放海康视频Cordova插件封装;

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。

 


欢迎关注微信公众号交流讨论!

 

 

 

 

  • 8
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值