.net 调用海康SDK实现NVR录像视频的下载

序言

上3篇海康SDK使用以及常见的坑受到了许多网友的喜爱,这也说明了在工控领域内,使用.net开发还是非常便捷省事的。 针对海康的SDK进行进一步封装,第一版Net Framework版本代码发在github上,供大家测试和使用。

这次主要讲解怎么实现从NVR硬盘录像机获取视频并下载保存。

45bc54a54cdf887719d1e834934845fa.png

声明下,海康威视没有给赞助费,希望厂家能够看到,给点打赏,哈哈~~~

1. 硬盘录像机下载流程

先翻阅SDK说明,可以看到流程如下:
610207cdd903ec966e77e1b254f9b324.png

初始化的过程在这里,不再讲解,大家可以去翻阅我之前的文章,有说明也有代码,可以拿来直接用。

我们直接切入主题,需要下载视频,那么一般需要知道硬盘录像机的通道。

登录到你的硬盘录像机,查看系统-通道管理,如下图所示。
5625bd4beaab6f302356cb4489afc933.png
通道号分别是 D1,D2,D3。数字是1,2,3。

好了,有了需要下载的通道,那么我们就可以枚举可以回放的录像文件了,通过SDK进行查找,会返回一堆文件名。

如果是硬盘录像机的界面,可以去查看回放的下载页面。
4021b7225f58de02d204774d809ce6f6.png
有了文件名,那么按照SDK NET_DVR_PlayBackControl_V40就可以去方便的下载文件了。

2. 机器人写下载程序

通过劈里啪啦的一堆键盘输出,很快写好了程序。 这段略去,我直接呼叫了ChatGPT。
00996e3bbb1dbdf957092f43d0c237ae.png

//谨慎参考,下列代码不是我写的,不代表本人的真实水平。
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;
    }
}

好了, 视频格式转换成功后,网页内播放完全没问题了。
fda59238c2a474735724c81e5541c011.png

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. 总结

视频录取效果超好,事件发生后,延迟最多几分钟,视频就完成了下载。
0a0dc4cc68ab6aa7da348506eb9d762c.png

希望这些介绍能帮助到大家,让大家从坑里跑出来,这块也没来的及进行整理,后续有时间了把这块代码优化下放到库里。

你学废了吗?

👓都看到这了,还在乎点个赞吗?

👓都点赞了,还在乎一个收藏吗?

👓都收藏了,还在乎一个评论吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要基于海康SDK实现网络硬盘录像机(NVR)的实时预览,可以使用Spring Boot框架来开发。首先,需要引入海康SDK的依赖,例如海康SDK提供的Java SDK。 在Spring Boot的配置文件中,配置海康SDK的相关参数,例如NVR的地址、端口号、用户名和密码等。这些参数可以通过配置文件的方式进行管理,方便后续维护和修改。 接着,在Spring Boot项目中创建一个Controller,用于处理实时预览的请求。在该Controller中,可以调用海康SDK提供的接口,进行NVR的登录。登录成功后,可以获取到NVR的实时预览的实时流地址。 然后,可以使用Spring Boot提供的Web Socket功能,实现实时流的推送。在Controller中,可以创建一个Web Socket连接,将实时流发送给前端页面。前端页面可以使用一些HTML5的标签和JavaScript库,例如video标签和Hls.js库,来实现实时预览的功能。 在Web Socket连接中,可以通过循环不断地从海康SDK获取实时流数据,并将数据发送给Web Socket连接。前端页面接收到数据后,可以将数据解析并显示在页面上,实现实时预览的效果。 最后,需要在Spring Boot项目中加入定时任务,定时检测NVR的状态,并在NVR断线或出现异常的情况下进行处理。可以将NVR的状态保存到数据库中,并在定时任务中检查NVR的状态,对异常状态进行处理,例如重新登录NVR或发送报警信息。 总之,通过使用Spring Boot框架和海康SDK,可以实现基于海康SDK的网络硬盘录像机NVR的实时预览功能。将海康SDK的接口与Spring Boot的功能相结合,可以实现更加稳定和高效的实时预览系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值