NvrSDK交接文档

这是使用md格式写成,为了方便阅读我就直接放到博客上了

一.工作内容

  • 外面客户购买了我们的NVR产品,需要提供SDK包做二次开发
  • 解答客户对接SDK过程中遇到的问题
  • 解决SDK本身存在的bug
  • 根据新的需求增加接口

总结起来就是:提供SDK安装包、解答对接、解决bug、新增需求接口;

二.准备工作

为了快速顺利的完成上述工作,需要的准备工作如下:

  • 了解NVRSDK发展历程
  • 熟悉NSIS打包
  • 熟悉接口使用
  • 熟悉代码框架
  • 钻研代码细节
  • 新增需求
  • 发现代码异味(来自《重构》),不断优化

以上每一步我都会下文中给出详细的讲解。

三.NVRSDK发展历程

NVRSDK是使用我公司NVR的通信库,NVRSDK第一版接口以NVR_前缀,同时包含了通信库和解码库的功能,现在只有日本方还在使用,而且已经由VC6迁移到了VC9,方便使用现有的底层库。工程所在目录为20161115_NVR_V5R2_JP/NVR_VOB/32-nvrclient,除此外,第一版SDK不再维护,统一提供第二版的NVRSDK,以NET_NVR_前缀。NVRSDK2.x分离了解码功能,另外封装了解码库DecodeSDK。

DecodeSDK因为刚开始提供的是cpp接口,即类接口,外面已经有些项目在使用,所以至今仍然保留了其打包脚本(打包下节会说到)。后来因为外面项目有使用各种语言(e.g. JAVA、delphi、C#),所以封装成c接口,工程为decodesdk_C.vcproj。如果是新客户,现在一律提供C接口版本。

NVRSDK2.x和DecodeSDK最新的工程目录为:
20160914_NVR_V5R2_Develop/NVR_VOB/32-nvrclient/nvrsdk2.6。nvrsdk2.6意为2016年创建。

四.NSIS打包

NVRSDK工程的编译脚本在nvrsdk2.6目录下的nvrsdk_setup.bat,打包脚本在NSIS目录下,打包须知如下:

  • 版本号控制:版本号以日期为号,如2016年12月5号提供的版本号为2.6.12.05,
    需要修改nsi打包脚本的!define PRODUCT_VERSION “2.6.12.05”
  • 由于历史原因当前打包有三个:
    I. nvrsdk.nsi打包ch文件夹,里面放的是C接口的解码库;
    II. nvrsdkcpp.nsi打包ch(c++)文件夹,里面放的是C++接口的解码库;
    III. nvrsdkcpp_en.nsi打包en文件夹,里面放的头文件和demo使用英文改写,
    解码库是C++接口的;
  • dll目录下请将pdb文件打包进去,方便外面崩溃后dump诊断;
  • 每次改动版本变更后,请在history文件夹下做好记录和备份;

五.熟悉接口使用

这部分内容请参考头文件、帮助文档、demo。大致接口调用流程图:

Created with Raphaël 2.1.2 初始化 NET_NVR_SDKInit DS_Init 登录 NET_NVR_LoginSync 获取设备列表 NET_NVR_GetAllDevice 实时浏览、录像回放、etc. 登出 NET_NVR_Logout 反初始化 NET_NVR_SDKCleanup DS_Quit

1. 初始化

一般库都有的初始化、反初始化、获取版本号、获取构建时间

- NET_NVR_SDKInit
- NET_NVR_SDKCleanup
- NET_NVR_GetSdkVersion
- NET_NVR_GetSdkBuildTime  

需要注意的是NET_NVR_SDKInit需要提供一个处理通知事件的回调函数,
客户比较感兴趣的几个通知事件是:

- NVR断链通知:SDK_NET_MSG_DISCONNECT_NVR
- 告警通知:SDK_NET_MSG_ALL_ALARM_NTY
- 前端设备状态变更通知:SDK_NET_MSG_DEV_STATUS_NTY
- 放像进度通知:SDK_NET_MSG_PLAYBACK_PROGRESS

2. 登录登出NVR

- NET_NVR_LoginSync
- NET_NVR_Logout

登录常见的错误码是#define SDK_NET_ERR_USER_OR_PSWD SKD_NET_ERR_BASE + 24
// 用户名或者密码错误

3. 实时浏览

Created with Raphaël 2.1.2 开始 获取连接NVR的本地IP NET_NVR_GetLocalIp 获取本地空闲端口 NET_NVR_GetLocalIdleStreamPort 获取视频重传端口 NET_NVR_GetEncoderVideoRetransPort 创建解码器 DS_CreateDecoder 设置解码器数据源网络参数 DS_SetNetParam 开始解码播放 DS_StartPlayStream 申请实时码流 NET_NVR_ApplyVideoStream 停止实时码流 NET_NVR_StopVideoStream 销毁解码器 DS_FreeDecoder 结束

4. 录像回放

录像回放的步骤与实时浏览类似,注意的是录像回放重传端口导致顺序有点区别:

Created with Raphaël 2.1.2 开始 获取连接NVR的本地IP NET_NVR_GetLocalIp 获取本地空闲端口 NET_NVR_GetLocalIdleStreamPort 创建解码器 DS_CreateDecoder 申请录像码流 NET_NVR_SingleRecordPlaybackSync 获取录像重传端口 NET_NVR_GetRecordRetransPort 设置解码器数据源网络参数 DS_SetNetParam 开始解码播放 DS_StartPlayStream 停止录像码流 NET_NVR_StopRecordPlayback 销毁解码器 DS_FreeDecoder 结束

录像查询(包括正常录像、告警录像、标签录像)相关接口:

- NET_NVR_QueryRecordSync
- NET_NVR_GetRecordItemCount
- NET_NVR_GetRecordItem
- NET_NVR_GetRecordAlarmCount
- NET_NVR_GetRecordAlarm
- NET_NVR_GetRecordTagCount
- NET_NVR_GetRecordTag

放像控制、录像下载、录像备份相关接口:

- NET_NVR_RecordPlaybackControl
- NET_NVR_DownloadRecord
- NET_NVR_StopDownloadRecord
- NET_NVR_RecordBackUp

5. PTZ云台控制

需要注意的是发送上下左右等移动命令后,只有当发送了停止命令(宏定义是eSDK_NET_PtzCommand_MOVESTOP)才会停止移动,界面开发时可以响应鼠标按下发送移动命令,鼠标弹起发送停止命令。

- NET_NVR_DevicePtzControl

6. NVR参数配置

包括基本参数、网络参数、NAT、串口、平台参数、系统时间、录像抓拍设置,etc.

- 获取NVR基本参数:NET_NVR_GetBasicSetting
- 设置NVR基本参数:NET_NVR_SetBasicSetting
- 获取NVR网络参数:NET_NVR_GetNetSetting
- 设置NVR网络参数:NET_NVR_SetNetSetting
- 获取NVR-NAT参数:NET_NVR_GetNatSetting
- 设置NVR-NAT参数:NET_NVR_SetNatSetting
- 获取NVR串口信息:NET_NVR_GetSerialPort
- 设置NVR串口信息:NET_NVR_SetSerialPort
- 获取NVR平台参数:NET_NVR_GetPlatfromSetting
- 设置NVR平台参数:NET_NVR_SetPlatfromSetting
- 获取NVR系统时间:NET_NVR_GetSystemTime
- 设置NVR系统时间:NET_NVR_SetSystemTime
- 获取录像抓拍参数:NET_NVR_GetRecordGrabSetting
- 设置录像抓拍参数:NET_NVR_SetRecordGrabSetting
- 获取邮箱服务:NET_NVR_GetEmailServer
- 设置邮箱服务:NET_NVR_SetEmailServer
- 测试邮箱服务:NET_NVR_CheckEmailServer

7. NVR系统管理

- 获取NVR版本信息:NET_NVR_GetVersion
- 获取NVR运行状态:NET_NVR_GetStatus
- 关机:NET_NVR_ShutDown
- 重启:NET_NVR_Reboot
- 恢复出厂:NET_NVR_RestoreFactory

8. 用户管理

- 增:NET_NVR_AddUser
- 删:NET_NVR_DeleteUser
- 改:NET_NVR_ModifyUser
- 查:NET_NVR_GetAllUserSync  

9. 日志管理

- NET_NVR_GetLog

10. 前端管理

- 增加前端设备:NET_NVR_AddDevice
- 删除前端设备:NET_NVR_DeleteDevice
- 获取前端设备列表:NET_NVR_GetAllDevice
- 获取所有前端设备基本信息:NET_NVR_GetAllDeviceBasicInfo
- 获取单个前端设备一般信息:NET_NVR_GetSingleDeviceInfo
- 获取设备能力集:NET_NVR_GetDeviceCapability
- 获取设备参数配置:NET_NVR_GetDeviceSetting
- 修改设备参数配置:NET_NVR_SetDeviceSetting
- 获取设备录像模式:NET_NVR_GetDeviceRecordMode
- 修改设备录像模式:NET_NVR_SetEncoderRecordMode

11. 告警管理

这部分涉及到告警联动较为复杂,告警分为系统告警(或者称为设备告警,e.g.设备上下线)、
业务告警(通过图像算法分析出的告警,e.g.移动侦测、图像遮蔽)

- 查询告警:NET_NVR_QueryAlarmData
- 获取系统告警处理:NET_NVR_GetServiceAlarmSetting
- 设置系统告警处理:NET_NVR_SetServiceAlarmSetting
- 获取告警输入开关状态:NET_NVR_GetAlarmInputSetting
- 设置告警输入开关状态:NET_NVR_SetAlarmInputSetting
- 获取业务告警联动: NET_NVR_GetServiceAlarmSetting
- 设置业务告警联动:NET_NVR_SetServiceAlarmSetting

12. 磁盘管理

包括盘组、磁盘、分区、配额信息

- 获取盘组表: NET_NVR_GetDiskGroupTable
- 设置盘组表: NET_NVR_SetDiskGroupTable
- 获取磁盘表: NET_NVR_GetDiskTable
- 获取磁盘运行时间: NET_NVR_GetDiskRunningTime
- 获取磁盘SMART信息: NET_NVR_GetDiskSmartInfo
- 获取分区表: NET_NVR_GetPartitionTable
- 修改分区设置: NET_NVR_ModifyPartitionSetting
- 格式化分区: NET_NVR_FormatPatition
- 获取配额表: NET_NVR_GetDiskQuotaTable
- 设置配额表: NET_NVR_SetDiskQuotaTable

13. 广播

- NET_NVR_StartCallorBroadcast
- NET_NVR_StopCallorBroadcast
- NET_NVR_SetCallorBroadcastVolumn
- NET_NVR_GetCallorBroadcastVolumn

14. 电视墙

- NET_NVR_LoadTvwall
- NET_NVR_StopTvwall

15. 智能模块

这一块还有待开发,目前已有的是查询智能分析图片接口、人流统计接口

- NET_NVR_QueryTracke
- NET_NVR_GetTrackeItemCount
- NET_NVR_GetTrackeItemInfo
- NET_NVR_StartStatistics
- NET_NVR_StopStatistics
- NET_NVR_GetStatisticsCount

16. 图片管理

- NET_NVR_CreatePictureDir
- NET_NVR_DeletePicture
- NET_NVR_UploadPicture
- NET_NVR_DownloadPicture
- NET_NVR_QueryPictureByDir

六.熟悉代码框架

nclib架构

NVRSDK建立在nclib的基础之上,nclib底层网络模块是用到了osp库,osp、nclib的学习案例可以参考王导写的,我这里只是简单的概述下。下面先贴出nclib的UML图:
NvrSDK交接文档_第1张图片

响应流程图:

Created with Raphaël 2.1.2 开始 nvr消息入口点 CNCIns::InstanceEntry 消息派发 CNCInsCfg::DispatchEvent 查找消息处理类 CManagerDisp::FindEventDisp 查找消息处理函数 CXXXDisp::DispEvent 消息处理函数 CXXXDisp::OnYYY 发送nclib消息 IDispEvent::SendEvent/PostEvent nclib消息处理函数 CNCLib::m_pNclibMsgProc

osp将每条TCP连接当做一个节点来处理,每个连接使用一个节点号进行通信。在InstanceEntry这个入口点会收到对应节点来的消息,消息号在evnvr.h中,消息携带的结构体信息在nvrstruct.h中由业务组定义。通过InstanceEntry中通过GetInsID获取到节点号,进而获取到对应的CNCInsCfg指针,这个类中保存了两个重要的指针,分别是消息派遣类CManagerDisp和数据保存类CNCLibData。CManagerDisp中根据业务对消息进行了一个分类,以防止类过于臃肿庞大。BuildEventMap通过宏REG_EVENT2DISP将消息号和具体处理类映射起来,通过FindEventDisp函数在map表中根据消息号寻找到处理类,DispEvent中再通过宏REG_ON_MESSAGE将消息号由对应函数来处理。

请求流程图:

Created with Raphaël 2.1.2 开始 获取派发管理类 CNCLib::GetManagerDisp 获取具体派发接口类 CManagerDisp::GetXXXDispInterface 具体业务 IXXXDisp::DoSomething 准备请求参数 IXXXDisp::PostReqToNvr 发出请求 CNCInsCfg::PostReqToNvr osp发送消息 ::OspPost

请求流程相对来说比较清晰简单,需要注意的是在PostReqToNvr可以设置等待的响应消息,以此判定是同步消息请求,在CNCInsCfg::PostReqToNvr中调用BeginWaitMsg对请求进行加锁,直到接收到对应的响应CheckIsWaitMsg才会调用StopWaitMsg解锁,这时才可以进行下一个请求。

NvrSDK架构

NvrSDK2.x版本因为需要用作服务器端,接受多线程的考验,而nclib层不是多线程安全的,这就要求NvrSDK多线程安全机制,而NvrSDK正是通过加了一层任务队列实现的。关键的两个类正是CTaskManager和CTaskInfo。

CTaskManager

1、主任务队列m_pNvrTaskDeque

所有任务请求都将通过PushTask到主任务队列中

2、处理ready任务线程HandleTask

从主任务队列中取出ready状态任务,调用StartTask开始做任务

3、处理done任务线程HandleDoneTask

从主任务队列中取出done或者error状态任务,主要处理异步任务做完后回调、同步任务超时后释放

4、nty任务队列m_pNvrNtyTaskDeque

因为考虑到主动上报消息(告警通知、放像进度etc.)过多可能会影响主任务队列的处理效率,所以额外创建了一个nty任务队列

5、处理nty任务线程HandleNtyTask

一般需要调用外部回调

6、CNCLib::m_pNclibMsgProc入口点:

s32 CTaskManager::NclibMsgProc(KEDA_NVRSDK::TMsgItem &tMsgItem)
    {
        CTaskManager* pTaskManager = CTaskManager::Instance();

        bool bDeal = FALSE;

        // 先处理放像进度、告警通知、ping通知(主动上报nty)
        if (tMsgItem.message == WM_NCLIB30_PLAY_PROGGRESS_NTY || 
            tMsgItem.message == WM_NCLIB_ALL_ALARM_NTY ||
            tMsgItem.message == WM_NCLIB_NVRTONC_PING_NTY ||
            tMsgItem.message == WM_NCLIB_STATISTICS_NTY ||
            tMsgItem.message == WM_NCLIB_REF_ALL_DEV_STA_NTY)
        {
            bDeal = pTaskManager->GetNtyTaskDeque()->DealNtyTask(tMsgItem);
            return bDeal;
        }

        // 轮询主任务队列中的nclibMsgProc
        bDeal = pTaskManager->GetTaskDeque()->DealTask(tMsgItem);
        if (bDeal)
            return bDeal;

        // 处理剩下的nty
        if (tMsgItem.wSerialNO == 0)
        {
            bDeal = pTaskManager->GetNtyTaskDeque()->DealNtyTask(tMsgItem);
            return bDeal;
        }

        SdkTraceWarning("nclibmsg not deal:message=%d\n", tMsgItem.message);
        return bDeal;
    }

7、PushTask

    BOOL CTaskManager::PushTask(ITaskInfo* pTaskInfo)
    {
        if (!m_pNvrTaskDeque->PushBackTask(pTaskInfo))
        {
            // 任务太多无法放入队列则丢掉
            CNvrSdkGlobal::Instance()->SetErrorCode(pTaskInfo->GetSerialNO(), SDK_NET_ERR_TASK_OVER);
            SdkTraceError("PushTask error:SDK_NET_ERR_TASK_OVER\n");
            delete pTaskInfo;
            return FALSE;
        }

        if (pTaskInfo->GetTaskExecuteMode() == ITaskInfo::EMTaskExecuteMode_Syn)
        {
            pTaskInfo->WaitSyn();
            SdkTraceDebug("DeleteSynTask %d\n", (int)(pTaskInfo->GetTaskFlag()));
            delete pTaskInfo; // 同步任务在等待结束后在此处删除任务
        }

        return TRUE;
    }

客户线程调用该接口将请求任务放入主任务队列,如果是同步任务会加锁,直到任务完成或错误或超时才会解锁。但是该锁只会阻塞住该客户线程,不会影响其它线程继续请求任务,所以NvrSDK实现了多线程处理任务并且安全。

CTaskInfo

OOD最重要的就是抽象类的设计,可以说一个框架搭的是否优秀,就是看它的抽象类设计的是否合理。抽象类就是定义了一种契约,约定了子类必须实现的接口,所以抽象类的接口是所有子类的共有特性和行为,不要把飞行的接口给到一个动物的抽象类,因为不是所有的动物都会飞,你可能会采取在爬行动物接口中覆盖飞行接口返回false,但这是一种愚蠢的设计。所以抽象类的每一个接口都应该深思熟虑是否适用于所有的子类。并且一定要遵守类的单一职责原则。

以这些标准来看CTaskInfo类还是过于臃肿了,身兼多职。即包含了多任务接口InsertTask、DelTask、DoNextTask等,又包含了同步任务和异步任务的划分,这点在后面的代码中可以发现极其繁琐,同时只有异步任务才需要用到回调接口来告诉客户任务处理的进度和结果,这些糅合在一起给实际的扩展和维护,可读性,都带来了不少的麻烦。因为不是所有的子类都是由多任务组合而成,有的已经可以确定是原子任务(不能再拆分的任务)了,而且大多数都是原子任务。原子任务和组合任务的关系有点类似控件与容器,容器可以容纳其它控件。在现有的需求下,我认为的任务抽象类应该如此纯洁:

class CTaskInfo{
public:
    CTaskInfo(TaskData tData) ;
    virtual ~CTaskInfo() ;

    // 具体任务子类需实现此方法调用IXXXDisp::DoSomething发出请求
    virtual bool doTask() = 0;
    // 实现此方法处理nclib响应消息
    virtual bool handleNclibMessage(TMsgItem tMsg) = 0;

    // 调用doTask,启动计时
    virtual bool startTask() ;

    // 任务状态变化后需要做的操作,我分为五种状态:准备态Ready、执行态Exec、完成态Done、错误态Error、子任务完成态SubDone(仅用于异步,可用来回调通知任务执行进度)
    virtual bool OnReady() ;
    virtual bool OnExec() ;
    virtual bool OnDone() ;
    virtual bool OnError() ;
    virtual bool OnSubDone() ;

    // 变化状态时调用对应OnStatus
    virtual bool setStatus(ETaskStatus eStatus) ;

    // 判断同步任务是否超时,返回对应状态
    virtual ETaskStatus getStatus() ;

    // 一些任务属性的get/set
    virtual DWORD getSerialNO() ;
    virtual ETaskFlag getTaskFlag() ;
    virtual WPARAM getWParam() ;
    virtual LPARAM getLParam() ;
    virtual void setParam(WPARAM wParam, LPARAM lParam) ;
    virtual int setErrCode() ;
    virtual void getErrCode() ;

protected:
    TaskData m_tData; // 任务创建时需要的数据
    DWORD m_dwSerialNO; // 任务流水号
    ETaskFlag m_eTaskFlag; // 任务标识
    WPARAM m_dwWParam;// 参数1
    LPARAM m_dwLParam;// 参数2
    ETaskStatus m_eTaskStatus; // 任务状态
    int m_iErrCode;// 错误码
    bool m_bAsync; // 是否是异步任务
};

接口我认为小写动词+大写名词的驼峰法是最能清晰表示的。
除了doTask(调用IXXXDisp中的接口发送任务请求)、handleNclibMessage(处理nclib消息),其它接口都可以在CTaskInfo中给出一般实现。所有继承自CTaskInfo的子类只需要实现doTask和handleNclibMessage即可。

而组合任务扩展自简单任务类:

class CTaskContainer : public CTaskInfo{
public:
    virtual ~CTaskContainer();

    // 重写startTask方法,来循环做子任务
    virtual bool startTask() ;

    bool addTask(TaskData tData);
    bool removeTask(CTaskInfo* pTask);
    CTaskInfo* getNextTask();

protected:
    vector m_vecTask;// 子任务组合
}

通过这样解释后,你应该能将NvrSdk的任务机制流程画出来了:

Created with Raphaël 2.1.2 开始 创建任务 CTaskFactory::CreateTaskByTaskFlag 将任务放入任务队列 CTaskManager::PushTask 处理任务 CTaskManager::HandleTask 开始任务(任务请求) ITaskInfo::StartTask nclib消息处理入口点 CTaskManager::NclibMsgProc 轮询任务队列nclib消息 CNvrTaskDeque::DealTask 由具体子类处理nclib消息(任务响应) ITaskInfo::nclibMsgProc 处理并移除完成任务 CTaskManager::HandleDoneTask 结束

注意的是这个流程不是在一条线程中完成的,客户线程调用PushTask放入任务,如果是同步任务则阻塞等待,由HandleTask线程去取任务做任务进行驱动,最后由HandleDoneTask线程取出并移除完成的任务。

七. 钻研代码细节

这部分内容要求你对某个具体任务子类请求和响应消息有个大致了解,下面以登录任务为例:
登录任务相当其它get/set参数任务来说,相当比较复杂,它由多任务组成,包括TCP连接、版本验证、用户名密码验证、同步数据到本地。

/*====================================================================
函 数 名:NET_NVR_LoginSync
功    能:NVR登录(同步方式)
参    数:pSerialTSDK_NVRLogin - [in] TSDK_NVRLogin 结构的串行化字符串
线程安全:是
返 回 值:成功,返回NVR ID (大于等于1)
          失败,返回SDK_NET_ERR_FAILED,通过NET_NVR_SDKGetLastError获取
          错误码
说    明:
====================================================================*/
NET_NVR_API int _STDCALL NET_NVR_LoginSync(const char *pSerialTSDK_NVRLogin)
{
    SdkTraceDebug("NET_NVR_LoginSync被调用\n");
    CSDKSerial cSerial(pSerialTSDK_NVRLogin);

    if (pSerialTSDK_NVRLogin == NULL)
    {
        CNvrSdkGlobal::Instance()->SetErrorCode(CNvrSdkGlobal::Instance()->GetCurThreadId(), SDK_NET_ERR_INVALID_PARAMS);
        return SDK_NET_ERR_FAILED;
    }

    TSDK_NVRLogin tLoginInfo;
    int nRet = DeserializationParam((char*)pSerialTSDK_NVRLogin, tLoginInfo);
    if (nRet != 0)
    {
        CNvrSdkGlobal::Instance()->SetErrorCode(GetCurrentThreadId(), SDK_NET_ERR_PARMETER);
        return SDK_NET_ERR_FAILED;
    }

    if (tLoginInfo.m_byAddrType == 1)
    {
        string strIP;
        u16 wPort;
        if (!GetIpAndPortByUrl(tLoginInfo.m_abyNvrUrl, strIP, wPort))
        {
            CNvrSdkGlobal::Instance()->SetErrorCode(GetCurrentThreadId(), SDK_NET_ERR_PARMETER);
            return SDK_NET_ERR_FAILED;
        }
        else
        {
            tLoginInfo.m_dwNVRIp = inet_addr(strIP.c_str());
            tLoginInfo.m_wLogInPort = wPort;
        }
    }

    CNvrData* pNvrData = CNvrSdkGlobal::Instance()->NewNvrData(tLoginInfo);
    if (pNvrData == NULL)
    {
        CNvrSdkGlobal::Instance()->SetErrorCode(GetCurrentThreadId(), SDK_NET_ERR_LOGIN_NVR_OVER);
        return SDK_NET_ERR_FAILED;  
    }

    int nNvrID = pNvrData->GetInsId();
    CNVR* pNvr = CNvrSdkGlobal::Instance()->CreateNvr(nNvrID);
    if (pNvr == NULL)
    {
        CNvrSdkGlobal::Instance()->SetErrorCode(CNvrSdkGlobal::Instance()->GetCurThreadId(), SDK_NET_ERR_NVR_NOT_EXIST);
        return SDK_NET_ERR_FAILED;
    }

    TUserInfo tUser;
    strcpy(tUser.m_achUserName, tLoginInfo.m_abyNvrUserName);
    strcpy(tUser.m_achLoginPwd, tLoginInfo.m_abyNvrPwd);
    TCREATETASKDATA tCreateTaskData;
    tCreateTaskData.emExecuteMode = ITaskInfo::EMTaskExecuteMode_Syn;
    tCreateTaskData.emTaskFlag = EMTaskFlag_LogIn;
    tCreateTaskData.nNvrID = nNvrID;
    tCreateTaskData.pSynParam1 = &tUser;
    tCreateTaskData.dwThreadId = GetCurrentThreadId();
    nRet = pNvr->Login(&tCreateTaskData);
    if (nRet != 0)
    {
        DWORD dwErr = CNvrSdkGlobal::Instance()->GetErrorCode(CNvrSdkGlobal::Instance()->GetCurThreadId());
        SdkTraceError("NET_NVR_LoginSync failed!ErrCode:%d\n", dwErr);
        CNvrSdkGlobal::Instance()->SetErrorCode(CNvrSdkGlobal::Instance()->GetCurThreadId(), dwErr);
        return SDK_NET_ERR_FAILED;
    }

    return nNvrID;
}
  1. 可以看到先是构造CSDKSerial对象,通过析构的方式保证序列化字符串被释放。判断输入参数是否有效合理,这是一个良好的习惯;
  2. CNvrSdkGlobal::Instance()->NewNvrData(tLoginInfo)实际是调用底层nclib去分配一个可用NVRID号;
  3. 然后调用CNvrSdkGlobal::Instance()->CreateNvr(nNvrID)创建CNVR对象;
  4. 填充TCREATETASKDATA任务数据,最终调用CNVR::Login,实际是将CTaskLogin放入任务队列中;
  5. 在CTaskLogin的SetParam中组合子任务CTaskConnect;
  6. CTaskConnect::DoTask中调用CNCLib::ConnectNVR进行TCP连接,在CTaskConnect::nclibMsgProc中处理WM_NCLIB_CONNECTNVR_NTY连接结果通知;
  7. 在CTaskLogin::DoTask中调用了CNCInsCfg::Login进行版本和用户名密码验证,在CTaskLogin::nclibMsgProc中处理WM_NCLIB_LOGIN_RES验证结果响应;
  8. 如果验证成功继而调用CNCLib::SyncLibData同步数据到本地,不断处理WM_NCLIB_SYNCDATA_STATE_NTY同步数据通知,直到SYNCDATA_SUCCEED同步数据全部完成。

这是登录任务比较繁琐,实际大多get/set参数任务都是很简单,以NET_NVR_GetEmailServer获取邮箱服务为例:

NET_NVR_API int _STDCALL NET_NVR_GetEmailServer(int nNvrID, void *pTSDK_EMailServer){
    if (!CNvrSdkGlobal::Instance()->IsValidInsId(nNvrID) || pTSDK_EMailServer == NULL)
    {
        return SDK_NET_ERR_INVALID_PARAMS;
    }

    TCREATETASKDATA tCreateTaskData;
    tCreateTaskData.emTaskFlag = EMTaskFlag_GetEmailServer;
    tCreateTaskData.emExecuteMode = ITaskInfo::EMTaskExecuteMode_Syn;
    tCreateTaskData.nNvrID = nNvrID;
    tCreateTaskData.pSynParam1 = pTSDK_EMailServer;
    CNVR* pNvr = CNvrSdkGlobal::Instance()->GetNVR(nNvrID);
    return pNvr->GetEmailServer(&tCreateTaskData);  
}
  1. 验证nNvrID、输入参数的有效性;
  2. 通过nNvrID获取到CNVR对象指针,调用对应接口GetEmailServer;
  3. 在CTaskGetEmailServer::DoTask中获取到INVRDisp指针,调用GetSystemParam,因为是直接从nclib本地获取的数据,所以都没有响应,CTaskGetEmailServer::nclibMsgProc中直接返回FALSE即可,即不处理任何响应。

八. 新增需求

  1. 业务组会在evnvr.h中添加请求和响应消息号,在nvrstruct.h中添加请求和响应消息结构体。
  2. 我们组(应用组)需要在msgdefine.h中添加nclib消息号
  3. 在对应分类IXXXDisp::BuildEventMap中注册派发类,在IXXXDisp::DispEvent中调用OnYYY的处理函数。
  4. 添加具体任务子类,实现doTask和handleNclibMessage
  5. 在CTaskFactory::CreateTaskByTaskFlag中添加任务产品的创建
  6. 在nvrsdk头文件中给出对外函数、结构体
  7. 在nvrsdk源文件中给出实现,即填充创建任务需要的数据

九. 优化代码

不要害怕去修改原代码,里面肯定是有很多烂代码的(包括我也可能埋下了很多坑,如果你发现了尽管吐槽好了,反正我也听不见),即使是以前良好的设计也有可能在需求变更下变得不合理了。只有通过不断的优化才能保证现在框架的可扩展性和稳定性。

我说下我发现的几处代码异味,有的我已经做出了优化,但有的还未来得及,如果你有时间可以尝试去优化,在这个过程中,你设计类和处理业务逻辑的能力都会得到很大的提升的。

  • CTaskInfo类可优化,但是现在具体任务子类已经快有上百个,改动抽象类修改量太大。
  • CTaskManager类还有优化余地,但也是受限于CTaskInfo类给出的接口,之前有过多线程多任务只使用一把同步任务锁导致多线程登录超时问题;
  • CNvrSdkGlobal严重违反单一职责原则,应该具体细分,之前就有过一把锁滥用导致死锁的问题;
  • CNVR类可以说是这个SDK中最大的败笔,本应先有对外的C++类接口,再封装成C接口,使用NVRID去映射对应的CNVR类,这个可以参考DecodeSDK的C接口实现,就是使用nDecoderID映射IDecoderSdk类。这个相当来说可以去优化改动,重新设计CNVR接口和实现,只是nvrsdk源文件实现需要调用CNVR接口,而现在C接口过多,所以修改量也不小;
  • 结构体序列化SerializeParam,这个是为了确保客户给的参数是对应的结构体,但我认为是多此一举,建议可以去掉。首先调用繁琐,还有之前就因为用于多线程中,有两处释放序列化字符串导致bug,还有如果接口实现中忘记添加CSDKSerial释放序列化字符串,会导致无序列化字符串可用;
  • 很多代码冗余,命名不规范,建议看看《代码简洁之道》

十.解码库简单介绍

解码库DecodeSDK现在统一提供C接口,解码数据源可以通过DS_SetNetParam设定为本地网络端口,也可以调用DS_PushRtpToDecode接口推送Rtp包。DS_CreateDecoder创建解码器时可以指定窗口句柄进行播放,如果仅用作回调不解码请将bOnlyFrmCB参数设为TRUE,这样不会创建解码线程和解码数据缓存,能大大减少CPU和内存占用。

解码库接口的一般调用流程图:

Created with Raphaël 2.1.2 初始化解码库 DS_Init 创建解码器 DS_CreateDecoder 设置网络参数 DS_SetNetParam 开始解码播放 DS_StartPlayStream 停止解码播放 DS_StopPlayStream 释放解码器 DS_FreeDecoder 释放解码库 DS_Quit

此外还有一些好用的接口:

  • 视频帧回调:DS_VideoFrameCB
  • 音频帧回调:DS_AudioFrameCB
  • YUV数据回调:DS_YuvDataCB(DS_CreateDecoder时需要解码
  • 抓拍:DS_PicSnapshot
  • 开始本地录像:DS_StartLocalRecord
  • 停止本地录像:DS_StopLocalRecord

解码库如果客户使用有问题,处理起来比较棘手,涉及到媒体组、网传组、解码驱动库。我们可以使用telnet日志进行一个初始的判断:

telnet 127.0.0.1 2600       打开telnet2600端口
pdinfo 8        打印解码日志
pdinfo 255      打印网传日志
decodershow     解码统计信息

十一. 常见客户问题记录

1、登录60024:用户名或密码错误,亦或版本认证失败;
2、登录60014:网络ping不通导致;
3、60178:任务超时 ;
4、60001:入参有误;
5、NET_NVR_GetLastError()获取当前线程最后一次错误,目前仅登录、申请实时码 流、申请录像码流接口用到;
6、由于解码库使用的默认四字节对齐(底层解码库是四字节对齐),网络库是一字节 对齐,所以使用其它开发语言定义头文件的请注意;(delphi的demo在交接的文件夹中可以找到
7、不安装打包,没拷贝解码驱动库,导致有码流无画面现象;
8、dxdiag诊断音频服务未启用,导致音频播放失败;

你可能感兴趣的:(工作)