383 lines
13 KiB
C#
383 lines
13 KiB
C#
|
|
using System.Runtime.InteropServices;
|
|||
|
|
using System.Text;
|
|||
|
|
using Microsoft.Extensions.Options;
|
|||
|
|
using YD_TrainingCompetition_Api.Hikvision;
|
|||
|
|
using YD_TrainingCompetition_Api.Models;
|
|||
|
|
using YD_TrainingCompetition_Api.Options;
|
|||
|
|
|
|||
|
|
namespace YD_TrainingCompetition_Api.Services;
|
|||
|
|
|
|||
|
|
public sealed class HikvisionIngestWorker : BackgroundService
|
|||
|
|
{
|
|||
|
|
// 静态实例引用,供静态回调使用
|
|||
|
|
private static HikvisionIngestWorker? s_instance;
|
|||
|
|
|
|||
|
|
private readonly ILogger<HikvisionIngestWorker> _logger;
|
|||
|
|
private readonly IHikvisionAlarmStore _alarmStore;
|
|||
|
|
private readonly IRealtimeNotifier _notifier;
|
|||
|
|
private readonly HikvisionOptions _options;
|
|||
|
|
private readonly List<CameraSession> _sessions = new();
|
|||
|
|
private bool _sdkInitialized;
|
|||
|
|
|
|||
|
|
public HikvisionIngestWorker(
|
|||
|
|
ILogger<HikvisionIngestWorker> logger,
|
|||
|
|
IHikvisionAlarmStore alarmStore,
|
|||
|
|
IRealtimeNotifier notifier,
|
|||
|
|
IOptions<HikvisionOptions> options)
|
|||
|
|
{
|
|||
|
|
_logger = logger;
|
|||
|
|
_alarmStore = alarmStore;
|
|||
|
|
_notifier = notifier;
|
|||
|
|
_options = options.Value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|||
|
|
{
|
|||
|
|
if (!_options.Enabled)
|
|||
|
|
{
|
|||
|
|
_logger.LogInformation("Hikvision SDK ingest is disabled");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s_instance = this;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 1. 初始化 SDK
|
|||
|
|
if (!CHCNetSDK.NET_DVR_Init())
|
|||
|
|
{
|
|||
|
|
_logger.LogError("NET_DVR_Init failed, error={Error}", CHCNetSDK.NET_DVR_GetLastError());
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
_sdkInitialized = true;
|
|||
|
|
_logger.LogInformation("NET_DVR_Init succeeded");
|
|||
|
|
|
|||
|
|
// 2. 设置 SDK 日志
|
|||
|
|
CHCNetSDK.NET_DVR_SetLogToFile(3, _options.SdkLogPath, true);
|
|||
|
|
|
|||
|
|
// 3. 设置本地配置:报警 JSON 图片分离
|
|||
|
|
SetLocalConfig();
|
|||
|
|
|
|||
|
|
// 4. 设置报警回调
|
|||
|
|
var callbackDelegate = new CHCNetSDK.MSGCallBack_V31(AlarmCallbackBridge);
|
|||
|
|
CHCNetSDK.NET_DVR_SetDVRMessageCallBack_V31(callbackDelegate, IntPtr.Zero);
|
|||
|
|
_logger.LogInformation("Alarm callback registered");
|
|||
|
|
|
|||
|
|
// 5. 登录所有启用的摄像头
|
|||
|
|
foreach (var cam in _options.Cameras.Where(c => c.Enabled))
|
|||
|
|
{
|
|||
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|||
|
|
await LoginCameraAsync(cam, stoppingToken);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_logger.LogInformation("Hikvision ingest running, {Count} camera(s) connected", _sessions.Count);
|
|||
|
|
|
|||
|
|
// 6. 保持运行,直到取消
|
|||
|
|
while (!stoppingToken.IsCancellationRequested)
|
|||
|
|
{
|
|||
|
|
await Task.Delay(5000, stoppingToken);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException)
|
|||
|
|
{
|
|||
|
|
_logger.LogInformation("Hikvision ingest worker cancelled");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "Hikvision ingest worker fatal error");
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
await CleanupAsync();
|
|||
|
|
s_instance = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void SetLocalConfig()
|
|||
|
|
{
|
|||
|
|
var localCfg = new CHCNetSDK.NET_DVR_LOCAL_GENERAL_CFG
|
|||
|
|
{
|
|||
|
|
byAlarmJsonPictureSeparate = _options.AlarmJsonPictureSeparate
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
int size = Marshal.SizeOf(localCfg);
|
|||
|
|
IntPtr ptr = Marshal.AllocHGlobal(size);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
Marshal.StructureToPtr(localCfg, ptr, false);
|
|||
|
|
if (!CHCNetSDK.NET_DVR_SetSDKLocalCfg(17, ptr))
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning("NET_DVR_SetSDKLocalCfg failed, error={Error}", CHCNetSDK.NET_DVR_GetLastError());
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
_logger.LogInformation("Local config set: AlarmJsonPictureSeparate={Val}", _options.AlarmJsonPictureSeparate);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
Marshal.FreeHGlobal(ptr);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task LoginCameraAsync(CameraConfigItem cam, CancellationToken ct)
|
|||
|
|
{
|
|||
|
|
var session = new CameraSession
|
|||
|
|
{
|
|||
|
|
DeviceId = cam.DeviceId,
|
|||
|
|
Ip = cam.Ip
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
var loginInfo = new CHCNetSDK.NET_DVR_USER_LOGIN_INFO
|
|||
|
|
{
|
|||
|
|
sDeviceAddress = Encoding.UTF8.GetBytes(cam.Ip.PadRight(129, '\0')),
|
|||
|
|
sUserName = Encoding.UTF8.GetBytes(cam.UserName.PadRight(64, '\0')),
|
|||
|
|
sPassword = Encoding.UTF8.GetBytes(cam.Password.PadRight(64, '\0')),
|
|||
|
|
wPort = cam.Port,
|
|||
|
|
bUseAsynLogin = false,
|
|||
|
|
byUseTransport = 0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
var deviceInfo = new CHCNetSDK.NET_DVR_DEVICEINFO_V40();
|
|||
|
|
|
|||
|
|
// 在后台线程执行同步登录
|
|||
|
|
int userId = await Task.Run(() =>
|
|||
|
|
CHCNetSDK.NET_DVR_Login_V40(ref loginInfo, ref deviceInfo), ct);
|
|||
|
|
|
|||
|
|
if (userId < 0)
|
|||
|
|
{
|
|||
|
|
uint err = CHCNetSDK.NET_DVR_GetLastError();
|
|||
|
|
_logger.LogError("Login failed: DeviceId={DeviceId}, Ip={Ip}, Error={Error}", cam.DeviceId, cam.Ip, err);
|
|||
|
|
|
|||
|
|
_alarmStore.UpdateCameraStatus(new HikvisionCameraStatus
|
|||
|
|
{
|
|||
|
|
DeviceId = cam.DeviceId,
|
|||
|
|
Ip = cam.Ip,
|
|||
|
|
Online = false,
|
|||
|
|
ErrorMessage = $"Login failed, error={err}"
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
session.UserId = userId;
|
|||
|
|
_logger.LogInformation("Login success: DeviceId={DeviceId}, Ip={Ip}, UserId={UserId}", cam.DeviceId, cam.Ip, userId);
|
|||
|
|
|
|||
|
|
// 设置布防
|
|||
|
|
var alarmParam = new CHCNetSDK.NET_DVR_SETUPALARM_PARAM
|
|||
|
|
{
|
|||
|
|
dwSize = (uint)Marshal.SizeOf<CHCNetSDK.NET_DVR_SETUPALARM_PARAM>(),
|
|||
|
|
byLevel = 0, // 0-一级报警(最高)
|
|||
|
|
byAlarmInfoType = 0,
|
|||
|
|
byDeployType = 1 // 1-实时布防
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
int alarmHandle = await Task.Run(() =>
|
|||
|
|
CHCNetSDK.NET_DVR_SetupAlarmChan_V41(userId, ref alarmParam), ct);
|
|||
|
|
|
|||
|
|
if (alarmHandle < 0)
|
|||
|
|
{
|
|||
|
|
uint err = CHCNetSDK.NET_DVR_GetLastError();
|
|||
|
|
_logger.LogWarning("SetupAlarmChan failed: DeviceId={DeviceId}, Error={Error}", cam.DeviceId, err);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
session.AlarmHandle = alarmHandle;
|
|||
|
|
session.Armed = true;
|
|||
|
|
_logger.LogInformation("Alarm armed: DeviceId={DeviceId}, Handle={Handle}", cam.DeviceId, alarmHandle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_sessions.Add(session);
|
|||
|
|
|
|||
|
|
_alarmStore.UpdateCameraStatus(new HikvisionCameraStatus
|
|||
|
|
{
|
|||
|
|
DeviceId = cam.DeviceId,
|
|||
|
|
Ip = cam.Ip,
|
|||
|
|
Online = true,
|
|||
|
|
Armed = session.Armed,
|
|||
|
|
UserId = userId,
|
|||
|
|
AlarmHandle = session.AlarmHandle
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 静态回调桥接 — SDK 回调在非托管线程上执行
|
|||
|
|
/// </summary>
|
|||
|
|
private static bool AlarmCallbackBridge(int lCommand, ref CHCNetSDK.NET_DVR_ALARMER pAlarmer, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser)
|
|||
|
|
{
|
|||
|
|
var instance = s_instance;
|
|||
|
|
if (instance == null) return true;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
instance.HandleAlarm(lCommand, ref pAlarmer, pAlarmInfo, dwBufLen);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
instance._logger.LogError(ex, "Unhandled error in alarm callback");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void HandleAlarm(int lCommand, ref CHCNetSDK.NET_DVR_ALARMER pAlarmer, IntPtr pAlarmInfo, uint dwBufLen)
|
|||
|
|
{
|
|||
|
|
if (lCommand != CHCNetSDK.COMM_ISAPI_ALARM)
|
|||
|
|
{
|
|||
|
|
// 仅处理 ISAPI 报警(JSON/XML 数据分离后的类型)
|
|||
|
|
_logger.LogDebug("Ignored alarm command: 0x{Command:X}", lCommand);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
string ip = GetString(pAlarmer.sDeviceIP);
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var struAlarm = Marshal.PtrToStructure<CHCNetSDK.NET_DVR_ALARM_ISAPI_INFO>(pAlarmInfo);
|
|||
|
|
|
|||
|
|
// 提取报警数据
|
|||
|
|
string alarmData = string.Empty;
|
|||
|
|
string dataType = "unknown";
|
|||
|
|
|
|||
|
|
if (struAlarm.dwAlarmDataLen > 0 && struAlarm.pAlarmData != IntPtr.Zero)
|
|||
|
|
{
|
|||
|
|
byte[] by = new byte[struAlarm.dwAlarmDataLen];
|
|||
|
|
Marshal.Copy(struAlarm.pAlarmData, by, 0, (int)struAlarm.dwAlarmDataLen);
|
|||
|
|
alarmData = Encoding.UTF8.GetString(by);
|
|||
|
|
|
|||
|
|
dataType = struAlarm.byDataType switch
|
|||
|
|
{
|
|||
|
|
1 => "xml",
|
|||
|
|
2 => "json",
|
|||
|
|
_ => "unknown"
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_logger.LogInformation(
|
|||
|
|
"[ISAPI Alarm] DeviceIp={Ip}, DataType={Type}, DataLen={Len}",
|
|||
|
|
ip, dataType, struAlarm.dwAlarmDataLen);
|
|||
|
|
|
|||
|
|
// 控制台输出
|
|||
|
|
Console.WriteLine($"========== ISAPI报警{dataType.ToUpper()}数据 (设备IP: {ip}) ==========");
|
|||
|
|
Console.WriteLine(alarmData);
|
|||
|
|
Console.WriteLine("==============================================================\r\n");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 提取图片数据
|
|||
|
|
var pictures = new List<AlarmPicture>();
|
|||
|
|
if (struAlarm.byPicturesNumber > 0 && struAlarm.pPicPackData != IntPtr.Zero)
|
|||
|
|
{
|
|||
|
|
int picStructSize = Marshal.SizeOf<CHCNetSDK.NET_DVR_ALARM_ISAPI_PICDATA>();
|
|||
|
|
for (int i = 0; i < struAlarm.byPicturesNumber; i++)
|
|||
|
|
{
|
|||
|
|
IntPtr picPtr = IntPtr.Add(struAlarm.pPicPackData, i * picStructSize);
|
|||
|
|
var struPic = Marshal.PtrToStructure<CHCNetSDK.NET_DVR_ALARM_ISAPI_PICDATA>(picPtr);
|
|||
|
|
|
|||
|
|
if (struPic.dwPicLen > 0 && struPic.pPicData != IntPtr.Zero)
|
|||
|
|
{
|
|||
|
|
byte[] picBytes = new byte[struPic.dwPicLen];
|
|||
|
|
Marshal.Copy(struPic.pPicData, picBytes, 0, (int)struPic.dwPicLen);
|
|||
|
|
|
|||
|
|
string fileName = Encoding.UTF8.GetString(struPic.szFilename).TrimEnd('\0');
|
|||
|
|
pictures.Add(new AlarmPicture
|
|||
|
|
{
|
|||
|
|
FileName = fileName,
|
|||
|
|
Length = struPic.dwPicLen,
|
|||
|
|
Base64 = Convert.ToBase64String(picBytes)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_logger.LogInformation("[ISAPI Alarm] Pictures count: {Count}", pictures.Count);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查找匹配的设备 ID
|
|||
|
|
string deviceId = _sessions
|
|||
|
|
.FirstOrDefault(s => string.Equals(s.Ip, ip, StringComparison.OrdinalIgnoreCase))
|
|||
|
|
?.DeviceId ?? ip;
|
|||
|
|
|
|||
|
|
// 创建报警记录
|
|||
|
|
var record = new AlarmRecord
|
|||
|
|
{
|
|||
|
|
DeviceId = deviceId,
|
|||
|
|
Ip = ip,
|
|||
|
|
DataType = dataType,
|
|||
|
|
RawData = alarmData,
|
|||
|
|
DataLength = struAlarm.dwAlarmDataLen,
|
|||
|
|
PictureCount = pictures.Count,
|
|||
|
|
Pictures = pictures,
|
|||
|
|
Timestamp = DateTimeOffset.UtcNow
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
_alarmStore.Add(record);
|
|||
|
|
|
|||
|
|
// 更新摄像头最后报警时间
|
|||
|
|
var status = _alarmStore.GetCameraStatus(deviceId);
|
|||
|
|
if (status != null)
|
|||
|
|
{
|
|||
|
|
status.LastAlarmTime = record.Timestamp;
|
|||
|
|
_alarmStore.UpdateCameraStatus(status);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通过 SignalR 推送报警数据到前端
|
|||
|
|
_ = _notifier.BroadcastHikvisionAlarmAsync(record);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "Error processing ISAPI alarm from {Ip}", ip);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task CleanupAsync()
|
|||
|
|
{
|
|||
|
|
foreach (var session in _sessions)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (session.AlarmHandle >= 0)
|
|||
|
|
{
|
|||
|
|
await Task.Run(() => CHCNetSDK.NET_DVR_CloseAlarmChan_V30(session.AlarmHandle));
|
|||
|
|
_logger.LogInformation("Alarm channel closed: DeviceId={DeviceId}", session.DeviceId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (session.UserId >= 0)
|
|||
|
|
{
|
|||
|
|
await Task.Run(() => CHCNetSDK.NET_DVR_Logout(session.UserId));
|
|||
|
|
_logger.LogInformation("Logout: DeviceId={DeviceId}", session.DeviceId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "Error cleaning up session: DeviceId={DeviceId}", session.DeviceId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_sessions.Clear();
|
|||
|
|
|
|||
|
|
if (_sdkInitialized)
|
|||
|
|
{
|
|||
|
|
await Task.Run(() => CHCNetSDK.NET_DVR_Cleanup());
|
|||
|
|
_logger.LogInformation("SDK cleanup completed");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetString(byte[] bytes)
|
|||
|
|
{
|
|||
|
|
if (bytes == null || bytes.Length == 0) return string.Empty;
|
|||
|
|
int nullIdx = Array.IndexOf(bytes, (byte)0);
|
|||
|
|
return nullIdx >= 0
|
|||
|
|
? Encoding.UTF8.GetString(bytes, 0, nullIdx)
|
|||
|
|
: Encoding.UTF8.GetString(bytes);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 摄像头会话状态
|
|||
|
|
/// </summary>
|
|||
|
|
private sealed class CameraSession
|
|||
|
|
{
|
|||
|
|
public string DeviceId { get; set; } = string.Empty;
|
|||
|
|
public string Ip { get; set; } = string.Empty;
|
|||
|
|
public int UserId { get; set; } = -1;
|
|||
|
|
public int AlarmHandle { get; set; } = -1;
|
|||
|
|
public bool Armed { get; set; }
|
|||
|
|
}
|
|||
|
|
}
|