这是使用md格式写成,为了方便阅读我就直接放到博客上了
总结起来就是:提供SDK安装包、解答对接、解决bug、新增需求接口;
为了快速顺利的完成上述工作,需要的准备工作如下:
来自《重构》
),不断优化以上每一步我都会下文中给出详细的讲解。
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年创建。
NVRSDK工程的编译脚本在nvrsdk2.6目录下的nvrsdk_setup.bat,打包脚本在NSIS目录下,打包须知如下:
这部分内容请参考头文件、帮助文档、demo。大致接口调用流程图:
一般库都有的初始化、反初始化、获取版本号、获取构建时间
- 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
- NET_NVR_LoginSync
- NET_NVR_Logout
登录常见的错误码是#define SDK_NET_ERR_USER_OR_PSWD SKD_NET_ERR_BASE + 24
// 用户名或者密码错误
录像回放的步骤与实时浏览类似,注意的是录像回放重传端口导致顺序有点区别:
录像查询(包括正常录像、告警录像、标签录像
)相关接口:
- 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
需要注意的是发送上下左右等移动命令后,只有当发送了停止命令(宏定义是eSDK_NET_PtzCommand_MOVESTOP
)才会停止移动,界面开发时可以响应鼠标按下发送移动命令,鼠标弹起发送停止命令。
- NET_NVR_DevicePtzControl
包括基本参数、网络参数、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
- 获取NVR版本信息:NET_NVR_GetVersion
- 获取NVR运行状态:NET_NVR_GetStatus
- 关机:NET_NVR_ShutDown
- 重启:NET_NVR_Reboot
- 恢复出厂:NET_NVR_RestoreFactory
- 增:NET_NVR_AddUser
- 删:NET_NVR_DeleteUser
- 改:NET_NVR_ModifyUser
- 查:NET_NVR_GetAllUserSync
- NET_NVR_GetLog
- 增加前端设备: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
这部分涉及到告警联动较为复杂,告警分为系统告警(或者称为设备告警,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
包括盘组、磁盘、分区、配额信息
- 获取盘组表: 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
- NET_NVR_StartCallorBroadcast
- NET_NVR_StopCallorBroadcast
- NET_NVR_SetCallorBroadcastVolumn
- NET_NVR_GetCallorBroadcastVolumn
- NET_NVR_LoadTvwall
- NET_NVR_StopTvwall
这一块还有待开发,目前已有的是查询智能分析图片接口、人流统计接口
- NET_NVR_QueryTracke
- NET_NVR_GetTrackeItemCount
- NET_NVR_GetTrackeItemInfo
- NET_NVR_StartStatistics
- NET_NVR_StopStatistics
- NET_NVR_GetStatisticsCount
- NET_NVR_CreatePictureDir
- NET_NVR_DeletePicture
- NET_NVR_UploadPicture
- NET_NVR_DownloadPicture
- NET_NVR_QueryPictureByDir
NVRSDK建立在nclib的基础之上,nclib底层网络模块是用到了osp库,osp、nclib的学习案例可以参考王导写的,我这里只是简单的概述下。下面先贴出nclib的UML图:
响应流程图:
osp将每条TCP连接当做一个节点来处理,每个连接使用一个节点号进行通信。在InstanceEntry这个入口点会收到对应节点来的消息,消息号在evnvr.h中,消息携带的结构体信息在nvrstruct.h中由业务组定义。通过InstanceEntry中通过GetInsID获取到节点号,进而获取到对应的CNCInsCfg指针,这个类中保存了两个重要的指针,分别是消息派遣类CManagerDisp和数据保存类CNCLibData。CManagerDisp中根据业务对消息进行了一个分类,以防止类过于臃肿庞大。BuildEventMap通过宏REG_EVENT2DISP将消息号和具体处理类映射起来,通过FindEventDisp函数在map表中根据消息号寻找到处理类,DispEvent中再通过宏REG_ON_MESSAGE将消息号由对应函数来处理。
请求流程图:
请求流程相对来说比较清晰简单,需要注意的是在PostReqToNvr可以设置等待的响应消息,以此判定是同步消息请求,在CNCInsCfg::PostReqToNvr中调用BeginWaitMsg对请求进行加锁,直到接收到对应的响应CheckIsWaitMsg才会调用StopWaitMsg解锁,这时才可以进行下一个请求。
NvrSDK2.x版本因为需要用作服务器端,接受多线程的考验,而nclib层不是多线程安全的,这就要求NvrSDK多线程安全机制,而NvrSDK正是通过加了一层任务队列实现的。关键的两个类正是CTaskManager和CTaskInfo。
所有任务请求都将通过PushTask到主任务队列中
从主任务队列中取出ready状态任务,调用StartTask开始做任务
从主任务队列中取出done或者error状态任务,主要处理异步任务做完后回调、同步任务超时后释放
因为考虑到主动上报消息(告警通知、放像进度etc.
)过多可能会影响主任务队列的处理效率,所以额外创建了一个nty任务队列
一般需要调用外部回调
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;
}
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实现了多线程处理任务并且安全。
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的任务机制流程画出来了:
注意的是这个流程不是在一条线程中完成的,客户线程调用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;
}
这是登录任务比较繁琐,实际大多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);
}
不要害怕去修改原代码,里面肯定是有很多烂代码的(包括我也可能埋下了很多坑,如果你发现了尽管吐槽好了,反正我也听不见
),即使是以前良好的设计也有可能在需求变更下变得不合理了。只有通过不断的优化才能保证现在框架的可扩展性和稳定性。
我说下我发现的几处代码异味,有的我已经做出了优化,但有的还未来得及,如果你有时间可以尝试去优化,在这个过程中,你设计类和处理业务逻辑的能力都会得到很大的提升的。
解码库DecodeSDK现在统一提供C接口,解码数据源可以通过DS_SetNetParam设定为本地网络端口,也可以调用DS_PushRtpToDecode接口推送Rtp包。DS_CreateDecoder创建解码器时可以指定窗口句柄进行播放,如果仅用作回调不解码请将bOnlyFrmCB参数设为TRUE,这样不会创建解码线程和解码数据缓存,能大大减少CPU和内存占用。
解码库接口的一般调用流程图:
此外还有一些好用的接口:
DS_CreateDecoder时需要解码
)解码库如果客户使用有问题,处理起来比较棘手,涉及到媒体组、网传组、解码驱动库。我们可以使用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诊断音频服务未启用,导致音频播放失败;