序言
上3篇海康SDK使用以及常见的坑受到了许多网友的喜爱,这也说明了在工控领域内,使用.net开发还是非常便捷省事的。 针对海康的SDK进行进一步封装,第一版Net Framework版本代码发在github上,供大家测试和使用。
这次主要讲解怎么实现从NVR硬盘录像机获取视频并下载保存。
声明下,海康威视没有给赞助费,希望厂家能够看到,给点打赏,哈哈~~~
1. 硬盘录像机下载流程
先翻阅SDK说明,可以看到流程如下:
初始化的过程在这里,不再讲解,大家可以去翻阅我之前的文章,有说明也有代码,可以拿来直接用。
我们直接切入主题,需要下载视频,那么一般需要知道硬盘录像机的通道。
登录到你的硬盘录像机,查看系统-通道管理,如下图所示。
通道号分别是 D1,D2,D3。数字是1,2,3。
好了,有了需要下载的通道,那么我们就可以枚举可以回放的录像文件了,通过SDK进行查找,会返回一堆文件名。
如果是硬盘录像机的界面,可以去查看回放的下载页面。
有了文件名,那么按照SDK NET_DVR_PlayBackControl_V40
就可以去方便的下载文件了。
2. 机器人写下载程序
通过劈里啪啦的一堆键盘输出,很快写好了程序。 这段略去,我直接呼叫了ChatGPT。
//谨慎参考,下列代码不是我写的,不代表本人的真实水平。
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace HikvisionNVRDownload
{
class Program
{
// 引入海康SDK的DLL
[DllImport("HCNetSDK.dll")]
public static extern bool NET_DVR_Init();
[DllImport("HCNetSDK.dll")]
public static extern int NET_DVR_Login_V30(string sDVRIP, int wDVRPort, string sUserName, string sPassword, ref NET_DVR_DEVICEINFO_V30 lpDeviceInfo);
[DllImport("HCNetSDK.dll")]
public static extern int NET_DVR_GetFileByTime(int lUserID, int lChannel, ref NET_DVR_TIME lpStartTime, ref NET_DVR_TIME lpStopTime, string sSavedFileName);
[DllImport("HCNetSDK.dll")]
public static extern bool NET_DVR_PlayBackControl(int lPlayHandle, uint dwControlCode, uint dwInValue, ref uint lpOutValue);
[DllImport("HCNetSDK.dll")]
public static extern bool NET_DVR_StopGetFile(int lPlayHandle);
[DllImport("HCNetSDK.dll")]
public static extern bool NET_DVR_Logout(int lUserID);
[DllImport("HCNetSDK.dll")]
public static extern bool NET_DVR_Cleanup();
// 设备信息结构体
[StructLayout(LayoutKind.Sequential)]
public struct NET_DVR_DEVICEINFO_V30
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
public string sSerialNumber;
public byte byAlarmInPortNum;
public byte byAlarmOutPortNum;
public byte byDiskNum;
public byte byDVRType;
public byte byChanNum;
public byte byStartChan;
public byte byAudioChanNum;
public byte byIPChanNum;
}
// 时间结构体
[StructLayout(LayoutKind.Sequential)]
public struct NET_DVR_TIME
{
public int dwYear;
public int dwMonth;
public int dwDay;
public int dwHour;
public int dwMinute;
public int dwSecond;
}
static void Main(string[] args)
{
// 初始化SDK
if (!NET_DVR_Init())
{
Console.WriteLine("初始化失败!");
return;
}
// 设备登录信息
string DVRIPAddress = "192.168.1.100"; // NVR的IP地址
int DVRPortNumber = 8000; // NVR的端口号
string DVRUserName = "admin"; // 用户名
string DVRPassword = "12345"; // 密码
NET_DVR_DEVICEINFO_V30 deviceInfo = new NET_DVR_DEVICEINFO_V30();
// 登录设备
int userID = NET_DVR_Login_V30(DVRIPAddress, DVRPortNumber, DVRUserName, DVRPassword, ref deviceInfo);
if (userID < 0)
{
Console.WriteLine("登录失败!");
return;
}
// 设置要下载的时间段
NET_DVR_TIME startTime = new NET_DVR_TIME
{
dwYear = 2024,
dwMonth = 9,
dwDay = 5,
dwHour = 0,
dwMinute = 0,
dwSecond = 0
};
NET_DVR_TIME stopTime = new NET_DVR_TIME
{
dwYear = 2024,
dwMonth = 9,
dwDay = 5,
dwHour = 23,
dwMinute = 59,
dwSecond = 59
};
// 下载视频文件保存路径
string savedFileName = @"C:\Downloads\channel1_20240905.mp4";
// 获取视频文件句柄
int playHandle = NET_DVR_GetFileByTime(userID, 1, ref startTime, ref stopTime, savedFileName); // 通道1
if (playHandle < 0)
{
Console.WriteLine("获取文件失败!");
NET_DVR_Logout(userID);
NET_DVR_Cleanup();
return;
}
// 开始下载
uint outValue = 0;
if (!NET_DVR_PlayBackControl(playHandle, 1, 0, ref outValue))
{
Console.WriteLine("下载控制失败!");
NET_DVR_StopGetFile(playHandle);
NET_DVR_Logout(userID);
NET_DVR_Cleanup();
return;
}
Console.WriteLine("文件正在下载,请稍候...");
// 这里可以添加进度控制的代码
// 结束下载
NET_DVR_StopGetFile(playHandle);
// 注销设备
NET_DVR_Logout(userID);
// 清理SDK
NET_DVR_Cleanup();
Console.WriteLine("下载完成!");
}
}
}
是的,按照这个写法,完成一个下载代码,就是分分钟的事情。
3.踩坑,填坑指南
3.1 想下载最新时间,但是NVR还没录取
是的,如果你的程序需要下载录取最新的视频,那么必然会面临一个问题,就是视频录取是需要时间的,而立即马上这件事情,可遇不可求。你需要等待NVR存盘后,才能真正的下载成功!
怎么办呢? 那就真的需要等待!
你需要增加一个线程,处理等待,如果查询到最新时间达到了你需要的时间段,那么才去启动下载程序。
using System;
public class HikvisionNVR
{
public static DateTime? GetLatestRecordingTime(int userID, int channel)
{
// 创建查找录像文件的条件
NET_DVR_FILECOND fileCond = new NET_DVR_FILECOND
{
lChannel = channel, // 通道号1
dwFileType = 0, // 文件类型 0-全部录像文件
dwIsLocked = 0, // 是否锁定 0-解锁文件
dwUseCardNo = 0, // 不使用卡号
};
// 查找录像文件
int findHandle = NET_DVR_FindFile_V30(userID, ref fileCond);
if (findHandle < 0)
{
Console.WriteLine("查找文件失败!");
return null;
}
NET_DVR_FINDDATA_V30 findData = new NET_DVR_FINDDATA_V30();
DateTime? latestTime = null;
// 读取文件列表,查找最新的文件
while (true)
{
int result = NET_DVR_FindNextFile_V30(findHandle, ref findData);
if (result == 1000) // 查找到文件
{
// 提取文件的结束时间作为最新时间
DateTime fileEndTime = new DateTime(
findData.struStopTime.dwYear,
findData.struStopTime.dwMonth,
findData.struStopTime.dwDay,
findData.struStopTime.dwHour,
findData.struStopTime.dwMinute,
findData.struStopTime.dwSecond
);
// 记录最新的文件时间
if (latestTime == null || fileEndTime > latestTime)
{
latestTime = fileEndTime;
}
}
else if (result == 1001) // 没有更多文件
{
break;
}
else if (result == -1) // 查询失败
{
Console.WriteLine("文件查找失败!");
break;
}
}
// 关闭查找句柄
NET_DVR_FindClose_V30(findHandle);
return latestTime;
}
}
3.2 多个线程同时下载报错
是的,海康NVR支持并行下载,但是有数量限制,如果你没有考虑到这一点,分分钟拉爆是很有可能的。
我们可以引入Semaphore
对象,来完成对并行下载的管理。
//优化下载,支持并发下载4个
Semaphore semaphore = new Semaphore(4, 4);
public bool DownloadFileConCurrent(int chnIdx, DateTime dateTimeStart, DateTime dateTimeEnd, string downloadPath, out string error)
{
try
{
error = "";
if (File.Exists(downloadPath))//已经下载
{
return true;
}
if (semaphore.WaitOne(60000))
{
try
{
var rtn = DownloadByTimeConCurrent(dateTimeStart, dateTimeEnd, chnIdx, downloadPath, out error);
return rtn;
}
finally
{
semaphore.Release();
}
}
else
{
return false;
}
}
catch (Exception ex)
{
error = "程序异常";
return false;
}
}
4. 终于可以写下载程序了
是的,前面的坑我们趟完了,就可以真正的干活了。
利用上面的流程,完成真正的下载。
以下代码纯手工打造,没有使用chatgpt。 不是使用它不好啊,是我当时笨…
private bool DownloadByTimeConCurrent(DateTime dateTimeStart, DateTime dateTimeEnd, long iSelIndex, string downloadPath, out string error)
{
error = "";
int downHandle = -1;
try
{
CHCNetSDK.NET_DVR_PLAYCOND struDownPara = new CHCNetSDK.NET_DVR_PLAYCOND();
struDownPara.dwChannel = (uint)iChannelNum[(int)iSelIndex - 1]; //通道号 Channel number
//设置下载的开始时间 Set the starting time
struDownPara.struStartTime.dwYear = (uint)dateTimeStart.Year;
struDownPara.struStartTime.dwMonth = (uint)dateTimeStart.Month;
struDownPara.struStartTime.dwDay = (uint)dateTimeStart.Day;
struDownPara.struStartTime.dwHour = (uint)dateTimeStart.Hour;
struDownPara.struStartTime.dwMinute = (uint)dateTimeStart.Minute;
struDownPara.struStartTime.dwSecond = (uint)dateTimeStart.Second;
//设置下载的结束时间 Set the stopping time
struDownPara.struStopTime.dwYear = (uint)dateTimeEnd.Year;
struDownPara.struStopTime.dwMonth = (uint)dateTimeEnd.Month;
struDownPara.struStopTime.dwDay = (uint)dateTimeEnd.Day;
struDownPara.struStopTime.dwHour = (uint)dateTimeEnd.Hour;
struDownPara.struStopTime.dwMinute = (uint)dateTimeEnd.Minute;
struDownPara.struStopTime.dwSecond = (uint)dateTimeEnd.Second;
//录像文件保存路径和文件名 the path and file name to save
//按时间下载 Download by time
downHandle = CHCNetSDK.NET_DVR_GetFileByTime_V40(m_lUserID, downloadPath, ref struDownPara);
if (downHandle < 0)
{
iLastErr = CHCNetSDK.NET_DVR_GetLastError();
error = "NET_DVR_GetFileByTime_V40 failed, error code= " + iLastErr;
try
{
if (File.Exists(downloadPath))
{
File.Delete(downloadPath);
}
}
catch { }
return false;//下载失败
}
uint iOutValue = 0;
if (!CHCNetSDK.NET_DVR_PlayBackControl_V40(downHandle, CHCNetSDK.NET_DVR_PLAYSTART, IntPtr.Zero, 0, IntPtr.Zero, ref iOutValue))
{
iLastErr = CHCNetSDK.NET_DVR_GetLastError();
error = "NET_DVR_PLAYSTART failed, error code2= " + iLastErr; //下载控制失败,输出错误号
return false;
}
//阻塞到下载完成
int iPos = 0;
while (iPos < 100)
{
iPos = CHCNetSDK.NET_DVR_GetDownloadPos(downHandle);
System.Threading.Thread.Sleep(500);
}
if (iPos == 100) //下载完成
{
if (!CHCNetSDK.NET_DVR_StopGetFile(downHandle))
{
iLastErr = CHCNetSDK.NET_DVR_GetLastError();
error = "NET_DVR_StopGetFile failed, error code= " + iLastErr; //下载控制失败,输出错误号
}
else
{
downHandle = -1;
}
}
if (iPos == 200) //网络异常,下载失败
{
try
{
if (File.Exists(downloadPath))
{
File.Delete(downloadPath);
}
}
catch { }
error = "网络异常,下载失败";
return false;
}
return true;
}
catch(Exception ex)
{
return false;
}
finally
{
try
{
if (downHandle >= 0)
{
StopDownload(downHandle);
}
}
catch { }
}
}
private void StopDownload(int downHandle)
{
if (downHandle < 0)
{
return;
}
if (!CHCNetSDK.NET_DVR_StopGetFile(downHandle))
{
iLastErr = CHCNetSDK.NET_DVR_GetLastError();
str = "NET_DVR_StopGetFile failed, error code= " + iLastErr; //下载控制失败,输出错误号
return;
}
}
5. 网页播放不了?
按照上面的每一步骤都走完了,运行后惊喜的发现,视频已经下载下来了,然而,放在网页上,竟然不能播放?
是的,海康的NVR下载的视频,不符合MP4的视频规范。
通俗的来说,音视频格式如 MP4,MP3等,其实并不严谨。.mp4其实是指封装格式,此封装格式支持多种音视频编码格式。mp4 封装格式可以支持的视频编码格式如 h264,h265, 音频格式如 PCM , aac等。
目前我们主流web 浏览器,支持良好的视频编码格式是H264, 音频格式是aac。 这也就是我们转换的目标。
而NVR源录像的编码格式视频格式多数可以在NVR中设置,目前主流的是H265.(相较于H264压缩比更高, 解码需要的计算资源也更高),音频编码是PCM。
这里需要请出ffmpeg程序了,它是一个开发中经常用到的音视频处理程序,经过测试,其转换H265编码 至 H264编码,还是相当耗时的,时效性基本在生产中无法接受, 转换音频编码效率较高。
因此我们在此处建议的方案是, 将NVR的视频编码格式直接指定为H264,这样视频流编码就不需要经过转换了。
以下通过 ffmpeg 将 a.dav 文件中的 视频编码保持编码格式,音频格式转换为 aac编码,同时使用 mp4容器封装。
ffmpeg -i a.dav -c:v copy -c:a aac 264.mp4
还是写个程序吧
private bool ConvertVideo(string source, string desc)
{
try
{
if (!File.Exists(source))
{
return false;
}
Process p = new Process();
p.StartInfo.FileName = ffmpeg;
string strArg = string.Format("-i {0} -c:v copy -c:a aac {1}", source, desc);
p.StartInfo.Arguments = strArg;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.CreateNoWindow = false;
p.ErrorDataReceived += new DataReceivedEventHandler(Output);
p.Start();//启动线程
p.BeginErrorReadLine();//开始异步读取
p.WaitForExit();//阻塞等待进程结束
p.Close();//关闭进程
p.Dispose();//释放资源
if (!File.Exists(desc))
{
return false;
}
return true;
}
catch (Exception ex)
{
return false;
}
}
好了, 视频格式转换成功后,网页内播放完全没问题了。
6. 好了,最后写个线程封装下
把下载的事件进行封装入队列,然后放在线程中进行枚举NVR通道最新文件的时间,如果没有达到时间要求,那么继续放在队列中。
# 初始化线程
private Thread _thread;
private ConcurrentQueue<CaptureVideoEvent> _queue = new ConcurrentQueue<CaptureVideoEvent>();
_thread = new Thread(DoWork);
_thread.IsBackground = true;
_thread.Name = "录取视频";
_thread.Start();
# 线程体
private void DoWork()
{
while (true)
{
if(_queue == null)
{
break;
}
if(_queue.TryDequeue(out var @event))
{
try
{
var path = @event.GetRelativePath(machine.MachineConfig.Id);
var dtMax = GetLatestRecordingTime(machine, @event);
if (!dtMax.HasValue)
{
continue;
}
if (dtMax.Value == DateTime.MinValue || dtMax<= @event.RecordTime)
{
if (DateTime.Now - @event.CreatedTime < TimeSpan.FromMinutes(5))
{
//下次处理
_queue.Enqueue(@event);
}
else
{
}
}
else
{
CaptureVideo(machine, @event);
}
}
catch(Exception ex)
{
Logger.Error($"云台 {@event.EquipmentId} 处理视频录取失败,{ex}");
}
}
Thread.Sleep(1000);
}
}
7. 总结
视频录取效果超好,事件发生后,延迟最多几分钟,视频就完成了下载。
希望这些介绍能帮助到大家,让大家从坑里跑出来,这块也没来的及进行整理,后续有时间了把这块代码优化下放到库里。
你学废了吗?
👓都看到这了,还在乎点个赞吗?
👓都点赞了,还在乎一个收藏吗?
👓都收藏了,还在乎一个评论吗?