YD_TrainingCompetition_Api/Services/HikvisionIngestWorker.cs
2026-07-02 10:05:51 +08:00

383 lines
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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; }
}
}