声明:本系列文章只提供交流与学习使用。文章中所有涉及到海康威视设备的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查看系统(二):服务器篇的更多相关文章

  1. 海康威视频监控设备Web查看系统(三):Web篇

    声明:本系列文章只提供交流与学习使用.文章中所有涉及到海康威视设备的SDK均可在海康威视官方网站下载得到.文章中所有除官方SDK以为的代码均可随意使用,任何涉及到海康威视公司利益的非正常使用由使用者自 ...

  2. 海康威视频监控设备Web查看系统(一):概要篇

    声明:本系列文章只提供交流与学习使用.文章中所有涉及到海康威视设备的SDK均可在海康威视官方网站下载得到.文章中所有除官方SDK意外的代码均可随意使用,任何涉及到海康威视公司利益的非正常使用由使用者自 ...

  3. 海康视频监控---Demo

    1,使用在页面中调用ActiveX控件 <object classid='clsid:E7EF736D-B4E6-4A5A-BA94-732D71107808' codebase='' stan ...

  4. C# 视频监控系列:学习地址汇总

    原文地址:http://www.cnblogs.com/over140/archive/2009/04/07/1429308.html 前言 对于视频监控系统大家应该是不陌生的,实施的路况信息.地铁. ...

  5. 视频监控——从其他浏览器打开低版本IE方案

    1. 方案背景 由于低版本IE浏览器并不支持很多新的页面技术,导致部分页面效果难以实现;另一方面IE浏览器版本与操作系统绑定,难以统一,不同版本IE间的不兼容导致多种兼容性问题,因此本项目暂定采用Ch ...

  6. 【转】C# 视频监控系列(12):H264播放器——播放录像文件

    原文地址:http://www.cnblogs.com/over140/archive/2009/03/23/1419643.html?spm=5176.100239.blogcont51182.16 ...

  7. 【转】C# 视频监控系列(13):H264播放器——控制播放和截图

    本文原文地址:http://www.cnblogs.com/over140/archive/2009/03/30/1421531.html 阿里云栖社区也有相关的视频开发案例:https://yq.a ...

  8. VSAM:视频监控系统 A System for Video Surveillance and Monitoring

    VSAM(VideoSurveillance and Monitoring)视频监控系统 Robotics Institute CMU 1:引言 2:试验床介绍 3:基本的视频分析算法:运动目标检测, ...

  9. Qt编写安防视频监控系统18-云台控制

    一.前言 云台控制是视频监控系统中必备的一个功能,对球机进行上下左右的移动,还有焦距的控制,其实核心就是控制XYZ三个坐标轴,为了开发这个模块,特意研究了各种云台控制的方法和开源库比如soap,有些厂 ...

随机推荐

  1. 【26.87%】【codeforces 712D】Memory and Scores

    time limit per test2 seconds memory limit per test512 megabytes inputstandard input outputstandard o ...

  2. Sql 将多个表查询的结果进行再次查询

    把你目前查到结果集定义为一个临时表 tempTable 下面是如何查 SELECT * FROM tempTable where 关键字=‘’举例 select book_num,book_name, ...

  3. Windows PowerShell 学习之——Cmdlet处理生命周期

    这一次介绍一下Cmdlet处理过程的生命周期 总共分为六个部分 1.概述 2. 命令行输入绑定参数(parameters) 3. 开始指令处理 4. 接受管道输入绑定参数 5. 处理记录 6. 处理记 ...

  4. mongdb aggregate 聚合数据

    最近用到的一些mongodb的数据查询方法 及api用法 Aggregate() 数据聚合处理的方法 可以将聚合的一些方法放在其后面的括号中,也可继续以agg.的样式链式加入 aggregate.al ...

  5. java构造器的作用

    通常通过在构造器中传入参数,对字段进行初始化,以达到初始化所创建的对象实例的目的.

  6. ios 拿到第一响应者的当前视图

    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; UIView *firstResponder = [keyWi ...

  7. 机器学习: DeepDreaming with TensorFlow (一)

    在TensorFlow 的官网上,有一个很有趣的教程,就是用 TensorFlow 以及训练好的深度卷积神经(GoogleNet)网络去生成一些有趣的pattern,通过这些pattern,可以更加深 ...

  8. DTFT、DFT、FFT

    对于一般的周期信号可以用一系列(有限个或者无穷多了)正弦波的叠加来表示.这些正弦波的频率都是某一个特定频率的倍数如5hz.2*5hz.3*5hz--(其中的 5hz 叫基频).这是傅立叶级数的思想.所 ...

  9. Symfony——如何使用Assetic实现资源管理

    1. 安装和启用 从Symfony 2.8开始,Assetic不再包含在Symfony Standard Edition中.在使用其任何功能之前,请在您的项目中安装执行此控制台命令的 AsseticB ...

  10. linq to entity DistinctBy && DefaultIfEmpty

    根据某属性去重 使用第三方库: https://github.com/morelinq/MoreLINQ Install-Package morelinq -Version 3.0.0 data.Di ...