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 _logger; private readonly IHikvisionAlarmStore _alarmStore; private readonly IRealtimeNotifier _notifier; private readonly HikvisionOptions _options; private readonly List _sessions = new(); private bool _sdkInitialized; public HikvisionIngestWorker( ILogger logger, IHikvisionAlarmStore alarmStore, IRealtimeNotifier notifier, IOptions 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(), 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 }); } /// /// 静态回调桥接 — SDK 回调在非托管线程上执行 /// 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(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(); if (struAlarm.byPicturesNumber > 0 && struAlarm.pPicPackData != IntPtr.Zero) { int picStructSize = Marshal.SizeOf(); for (int i = 0; i < struAlarm.byPicturesNumber; i++) { IntPtr picPtr = IntPtr.Add(struAlarm.pPicPackData, i * picStructSize); var struPic = Marshal.PtrToStructure(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); } /// /// 摄像头会话状态 /// 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; } } }