YD_TrainingCompetition_Api/Services/HikvisionIngestWorker.cs

383 lines
13 KiB
C#
Raw Permalink Normal View History

2026-07-02 10:05:51 +08:00
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; }
}
}