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