海康威视频监控设备Web查看系统(二):服务器篇
声明:本系列文章只提供交流与学习使用。文章中所有涉及到海康威视设备的SDK均可在海康威视官方网站下载得到。文章中所有除官方SDK意外的代码均可随意使用,任何涉及到海康威视公司利益的非正常使用由使用者自己负责,与本人无关。
前言:
上一篇文章《海康威视频监控设备Web查看系统(一):概要篇》笼统的介绍了关于海康视频中转方案的思路,本文将一步步实现方案中的视频中转服务端。文中会涉及到一些.net socket处理和基础的多线程操作。我用的是SDK版本是SDK_Win32_V4.2.8.1 。大家根据自己实际情况想在相应的SDK,页面的说明里有详细的设备型号列表。
分析官方SDK的Demo:
首先来看看官方SDK中的C#版本的Demo,官方Demo分为两个版本,分别是“实时预览示例代码一”和“实时预览示例代码二”,因为有现成的C#版本,所以我们使用示例代码一中的内容。首先关注名为CHCNetSDK的类,这个类封中装了SDK中的所有非托管方法接口,我们需要来把这个类以及SDK中的DLL文件一起引入到我们的项目中,如果有对C#调用C++类库不了解的朋友请自己Google一下,资料非常多,博客园里也有很多作者写过这一类的文章,本文就不就这个内容做深入讨论。
调用SDK没有问题了,接下来看看SDK的使用,根据SDK使用文档,SDK接口的调用需要通过一个标准流程,流程图如下:
按照这个流程,我们第一步要做的是初始化SDK,然后是三个可选回调函数的设置,接着要做用户注册设备即设备登录,紧接着就是核心的部分了,根据上一篇文章中讲的思路,除了预览模块外其他几个模块的调用不在我们要解决的问题范畴,因此不予考虑。最后一步是注销设备,释放SDK资源。所以,最后根据我们的需求,流程简化如下:
虽然标准流程如此,但是我们的服务端程序只有一个单一的任务,所以也没有必要对为托管资源进行释放,因为如果退出程序以后资源就会释放,不退出程序的话,SDK资源就不应该被释放。因此再简化一下流程每个节点都有相应的代码实现如如下所示:
//初始化SDK
CHCNetSDK.NET_DVR_Init(); //用户登录
CHCNetSDK.NET_DVR_DEVICEINFO_V30 DeviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V30();
CHCNetSDK.NET_DVR_Login_V30(设备IP地址, 设备端口, 用户名, 密码, ref DeviceInfo);
//说明:关于设备IP、端口、用户名及密码信息请根据自己要访问设备的设置正确填写 //预览模块
CHCNetSDK.NET_DVR_CLIENTINFO lpClientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO();
lpClientInfo.lChannel = channel;
lpClientInfo.lLinkMode = 0x0000;
lpClientInfo.sMultiCastIP = "";
m_fRealData = new CHCNetSDK.REALDATACALLBACK(RealDataCallBack);
IntPtr pUser = new IntPtr();
CHCNetSDK.NET_DVR_RealPlay_V30(m_lUserID, ref lpClientInfo, m_fRealData, pUser, );
//说明:这里的NET_DVR_CLIENTINFO类中缺少预览窗口的句柄,需要预览时,要根据自己的项目设置NET_DVR_CLIENTINFO对象的hPlayWnd属性
可能有朋友看到这里已经忍受不了了,说好的视频中转功能在哪呢?别着急,一切的处理都在回调函数RealDataCallBack中,先耐心看一下这个回调函数的签名
void RealDataCallBack(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser)
第一个lRealHandle是预览控件的句柄,第二个参数dwDataType说明回调接收到的数据类型,pBuffer 存放数据的缓冲区指针, dwBufSize 缓冲区大小 ,pUser 用户数据的句柄。我做的这个视频的中转功能其实就是在这个回调函数中实现的。
好了,核心的代码都摘出来了,大家按照SDK提供的Demo照猫画虎就可以把预览功能实现出来了。
服务端设计:
实现了预览功能,下面看看中转服务的实现。其中包含三个类:Server,Client以及ClientList类。
Server类主要负责从设备读取数据并将数据缓存到服务器上,并且作为Socket监听服务端;ClientList维护一个客户端列表,并在Server获取到设备数据时便利客户端列表发送数据到客户端;Client类主要负责将服务端缓存的数据分发到各个终端请求上。
三个类的关系及主要成员请看下图:
Server类:
class Server
{
int m_lUserID = -;
//头数据
byte[] headStream; ClientList clientList = ClientList.GetClientList();
CHCNetSDK.REALDATACALLBACK m_fRealData;
Socket listenSocket;
Semaphore m_maxNumberAcceptedClients;
/// <summary>
/// Server构造函数,启动服务端Socket及海康SDK获取设备数据
/// </summary>
/// <param name="ipPoint">服务端IP配置</param>
/// <param name="numConnections">最大客户端连接数</param>
/// <param name="channel">设备监听通道</param>
public Server(IPEndPoint ipPoint, int numConnections, int channel)
{
if (!InitHK())
{
return;
}
RunGetStream(channel); listenSocket = new Socket(ipPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
listenSocket.Bind(ipPoint);
m_maxNumberAcceptedClients = new Semaphore(numConnections, numConnections);
listenSocket.Listen();
Console.WriteLine("开始监听客户端连接......");
StartAccept(null);
} #region HKSDK private void RunGetStream(int channel)
{
if (m_lUserID != -)//初始化成功
{
CHCNetSDK.NET_DVR_CLIENTINFO lpClientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO();
lpClientInfo.lChannel = channel;
lpClientInfo.lLinkMode = 0x0000;
lpClientInfo.sMultiCastIP = "";
m_fRealData = new CHCNetSDK.REALDATACALLBACK(RealDataCallBack);
IntPtr pUser = new IntPtr();
int m_lRealHandle = CHCNetSDK.NET_DVR_RealPlay_V30(m_lUserID, ref lpClientInfo, m_fRealData, pUser, );
Console.WriteLine("开始获取视频数据......");
}
else//初始化 失败,因为已经初始化了
{
Console.WriteLine("视频数据获取失败......");
}
} private bool InitHK()
{
bool m_bInitSDK = CHCNetSDK.NET_DVR_Init();
if (m_bInitSDK == false)
{
return false;
}
else
{
Console.WriteLine("设备SDK初始化成功.......");
CHCNetSDK.NET_DVR_DEVICEINFO_V30 DeviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V30();
m_lUserID = CHCNetSDK.NET_DVR_Login_V30("设备IP", 连接端口, "连接用户名", "连接密码", ref DeviceInfo);
if (m_lUserID != -)
{
Console.WriteLine("监控设备登录成功.......");
return true;
}
else
{
Console.WriteLine("监控设备登录失败,稍后再试.......");
return false;
}
}
} private void RealDataCallBack(Int32 lRealHandle, UInt32 dwDataType, IntPtr pBuffer, UInt32 dwBufSize, IntPtr pUser)
{
byte[] data = new byte[dwBufSize];
Marshal.Copy(pBuffer, data, , (int)dwBufSize);
Console.WriteLine("监控设备连接正常......");
if (dwDataType == CHCNetSDK.NET_DVR_SYSHEAD)
{
headStream = data;
}
clientList.SetSendData(data);
return;
} #endregion #region Socket
/// <summary>
/// 监听客户端
/// </summary>
/// <param name="acceptEventArg"></param>
private void StartAccept(SocketAsyncEventArgs acceptEventArg)
{
if (acceptEventArg == null)
{
acceptEventArg = new SocketAsyncEventArgs();
acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed);
}
else
{
acceptEventArg.AcceptSocket = null;
} m_maxNumberAcceptedClients.WaitOne();
bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArg);
if (!willRaiseEvent)
{
ProcessAccept(acceptEventArg);
}
}
/// <summary>
/// 增加客户端列表
/// </summary>
/// <param name="e"></param>
private void ProcessAccept(SocketAsyncEventArgs e)
{
clientList.AddClient(new Client(e.AcceptSocket, headStream));
StartAccept(e);
} /// <summary>
/// Socket回调函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void IO_Completed(object sender, SocketAsyncEventArgs e)
{
switch (e.LastOperation)
{
case SocketAsyncOperation.Accept:
ProcessAccept(e);
break;
default:
throw new ArgumentException("The last operation completed on the socket was not a receive or send");
}
} #endregion }
ServerClass
这里有个细节问题要说明一下,当服务端每次注册到设备时,设备第一次返回的数据里面的前40个字节是头数据,在解码阶段时需要将这40字节数据先发送给解码程序,否则解码程序将无法正常操作。所以在Server类中单独保存了这40字节的头数据以备分发给各个客户端。
另外,由于我们的客户端只需要不停的从服务端接收数据,所以服务端设计时只需要将数据分发给客户端即可,无需在Server类中维护客户端状态,因此,服务端Socket只进行监听操作,当监听到有客户端连接时,将客户端连接添加到ClientList即可。下面看看ClientList类的实现:
class ClientList
{
private static ClientList list = null;
private ClientList() { }
private List<Client> socketList = new List<Client>(); /// <summary>
/// 获取ClientList单例
/// </summary>
/// <returns></returns>
public static ClientList GetClientList()
{
if (list == null)
list = new ClientList();
return list;
}
/// <summary>
/// 将客户端增加到ClientList中
/// </summary>
/// <param name="client"></param>
public void AddClient(Client client)
{
this.socketList.Add(client);
}
/// <summary>
/// 遍历发送数据到客户端
/// </summary>
/// <param name="data"></param>
public void SetSendData(byte[] data)
{
socketList.RemoveAll((s) => { return s.SocketError != SocketError.Success; });
PerformanceCounter p = new PerformanceCounter("Processor", "% Processor Time", "_Total");
for (int i = ; i < socketList.Count; i++)
{
socketList[i].SetData(data);
if (p.NextValue() > )
Thread.Sleep();
}
}
}
ClientListClass
在SetSendData方法中遍历客户端列表发送数据时,用到了PerformanceCounter对象来控制服务器CPU的使用率,防止CPU资源过载。在实际运行过程中需要对PerformanceCounter对象获取的使用率的条件和线程等待时间做适当的微调来达到想要的效果。我这里的参数是我在PC Server上部署的时候采用的,如果是高CPU配置的话,需要把CPU使用率的判断条件改小一些,否则会出现服务端单次从设备读取数据时间过长的问题,在客户端显示时出现延时。
最后看看Client类的实现:
class Client
{
/// <summary>
/// 客户端连接Socket
/// </summary>
private Socket socket;
/// <summary>
/// 发送的数据类型
/// </summary>
private BufferType type = BufferType.Head;
/// <summary>
/// 头数据
/// </summary>
private byte[] headStream;
private SocketError socketError = SocketError.Success;
/// <summary>
/// 控制数据发送顺序信号量
/// </summary>
private ManualResetEvent sendManual = new ManualResetEvent(false);
private byte[] sendData;
/// <summary>
/// 发送数据线程
/// </summary>
private Thread sendThread;
/// <summary>
/// 客户端构造函数
/// </summary>
/// <param name="socket"></param>
/// <param name="headStream"></param>
public Client(Socket socket, byte[] headStream)
{
this.headStream = headStream;
this.socket = socket;
sendThread = new Thread((object arg) =>
{ while (true)
{
sendManual.WaitOne();
if (socketError == SocketError.Success)
{
try
{
Console.WriteLine(sendData.Length);
socket.Send(sendData);
}
catch (Exception)
{
Distroy();
break;
} }
sendManual.Reset();
}
});
sendThread.IsBackground = true;
sendThread.Start();
}
/// <summary>
///
/// </summary>
public SocketError SocketError
{
get
{
return socketError;
}
}
/// <summary>
///
/// </summary>
/// <param name="data"></param>
public void SetData(byte[] data)
{
if (this.socketError != SocketError.Success)
{
return;
}
if (type == BufferType.Head && headStream.Length == )
{
sendData = headStream;
type = BufferType.Body;
}
else
{
sendData = data;
}
sendManual.Set();
}
/// <summary>
/// 销毁Client对象,释放资源
/// </summary>
private void Distroy()
{
this.sendThread.Abort();
this.socket.Shutdown(SocketShutdown.Both);
this.socket.Dispose();
this.socketError = SocketError.ConnectionRefused;
}
} enum BufferType
{
Head, Body
}
ClientClass
简要说明一下,因为中转服务的一直处于大量连接数据的发送过程中,所以在Client的构造函数中为每一个实例开了一个本地线程作为数据发送的处理线程,而不是使用线程池来做处理。另外,使用ManualResetEvent实例作为信号量来控制Client实例在发送数据时是按照Server实例从设备采集的数据的顺序来一条一条发送的,这样避免了由于数据流混乱造成的客户端解码时出现解码错误或者跳帧等现象。
好了,视频中转服务器端的程序已经开发出来了,接下来要做的就是做一个Web插件来接收服务端的数据并解码播放,这些内容留作下一篇内容。敬请关注!
海康威视频监控设备Web查看系统(二):服务器篇的更多相关文章
- 海康威视频监控设备Web查看系统(三):Web篇
声明:本系列文章只提供交流与学习使用.文章中所有涉及到海康威视设备的SDK均可在海康威视官方网站下载得到.文章中所有除官方SDK以为的代码均可随意使用,任何涉及到海康威视公司利益的非正常使用由使用者自 ...
- 海康威视频监控设备Web查看系统(一):概要篇
声明:本系列文章只提供交流与学习使用.文章中所有涉及到海康威视设备的SDK均可在海康威视官方网站下载得到.文章中所有除官方SDK意外的代码均可随意使用,任何涉及到海康威视公司利益的非正常使用由使用者自 ...
- 海康视频监控---Demo
1,使用在页面中调用ActiveX控件 <object classid='clsid:E7EF736D-B4E6-4A5A-BA94-732D71107808' codebase='' stan ...
- C# 视频监控系列:学习地址汇总
原文地址:http://www.cnblogs.com/over140/archive/2009/04/07/1429308.html 前言 对于视频监控系统大家应该是不陌生的,实施的路况信息.地铁. ...
- 视频监控——从其他浏览器打开低版本IE方案
1. 方案背景 由于低版本IE浏览器并不支持很多新的页面技术,导致部分页面效果难以实现;另一方面IE浏览器版本与操作系统绑定,难以统一,不同版本IE间的不兼容导致多种兼容性问题,因此本项目暂定采用Ch ...
- 【转】C# 视频监控系列(12):H264播放器——播放录像文件
原文地址:http://www.cnblogs.com/over140/archive/2009/03/23/1419643.html?spm=5176.100239.blogcont51182.16 ...
- 【转】C# 视频监控系列(13):H264播放器——控制播放和截图
本文原文地址:http://www.cnblogs.com/over140/archive/2009/03/30/1421531.html 阿里云栖社区也有相关的视频开发案例:https://yq.a ...
- VSAM:视频监控系统 A System for Video Surveillance and Monitoring
VSAM(VideoSurveillance and Monitoring)视频监控系统 Robotics Institute CMU 1:引言 2:试验床介绍 3:基本的视频分析算法:运动目标检测, ...
- Qt编写安防视频监控系统18-云台控制
一.前言 云台控制是视频监控系统中必备的一个功能,对球机进行上下左右的移动,还有焦距的控制,其实核心就是控制XYZ三个坐标轴,为了开发这个模块,特意研究了各种云台控制的方法和开源库比如soap,有些厂 ...
随机推荐
- leetcode先刷_Valid Sudoku
我没有看到这个问题,这使其在现货需求数独,害怕一直没敢做.后来我发现原来的标题就是这么简单.推断现在只有数字全不符合的就可以了棋盘上的形势的要求. 是不是正确的三个周期..人是不能满意地看到每一行.每 ...
- CodeBlocks提供了预编译的WxWidgets模块,并预置TDM
Miscellaneous For Windows, we also provide the pre-compiled wxWidgets, version 2.8.12 used to compil ...
- Eclipse 学习总结
一. Eclipse 中一个普通 JavaWeb 项目的目录结构 如果项目工程中没有web.xml文件,可以手动动态添加. 右击项目 -> java EE Tools -> ...
- Dojo第一节:学会使用firebug对js,Dojo进行调适
内容概要: 学会使用firebug的基本功能 1. 简介:Firebug是Firefox的一个插件,用来对js代码进行调适的工具. (官方废话:Firebug是firefox下的一个插件,可以调试全部 ...
- Xcode7.1 网络请求报错
The resource could not be loaded because the App Transport Security policy reguir 原因:iOS9引入了新特性App T ...
- prototype __proto__ Function
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象.(注意:是函数才有prototype属性) 而__proto__属性每一个对象都有. 在js中如果A对象是由B函数构 ...
- zedboard之GPIO驱动器(离FPGA直到LINUX申请书)
笔者:xiabodan 资源: http://blog.csdn.net/xiabodan/article/details/24308373 1 EDK 大家知道我们在EDK中建立GPIO然后倒出 ...
- 3D场景中的鼠标响应事件
原文:3D场景中的鼠标响应事件 今天要讲的是3D场景中的鼠标响应事件的处理,首先Button的响应是大家熟知的,只要加上一个click事件,然后写一个响应的处理时间就行了.对于二维平面上的一些控件也很 ...
- nyoj116士兵杀死(两)段树单点更新
士兵杀敌(二) 时间限制:1000 ms | 内存限制:65535 KB 难度:5 描写叙述 南将军手下有N个士兵,分别编号1到N,这些士兵的杀敌数都是已知的. 小工是南将军手下的军师,南将军常常 ...
- 使用Struts2的iterator标签遍历复杂Map种类
1.建一个Webproject.加入Struts2支持. 2.创建两个实体类: a). Mother(母亲)的Java类. package struts.map.entity; import java ...