using Microsoft.AspNetCore.Mvc; namespace SHH.CameraSdk; [ApiController] [Route("api/[controller]")] public class CamerasController : ControllerBase { private readonly CameraManager _manager; // 1. 新增:我们需要配置管理器 private readonly ProcessingConfigManager _configManager; // 构造函数注入管理器 public CamerasController(CameraManager manager, DisplayWindowManager displayManager, ProcessingConfigManager configManager) { _manager = manager; _displayManager = displayManager; _configManager = configManager; } // ========================================================================== // 区域 A: 设备全生命周期管理 (CRUD) // ========================================================================== /// /// 1. 获取所有设备清单 /// [HttpGet] public IActionResult GetAll() { var devices = _manager.GetAllDevices().Select(d => new { d.Id, d.Config.Name, d.Config.RenderHandle, d.Config.StreamType, d.Config.IpAddress, Brand = d.Config.Brand.ToString(), Status = d.Status.ToString(), d.RealFps, d.TotalFrames, d.IsPhysicalOnline, d.IsOnline, d.IsRunning, d.Width, d.Height, d.PreprocessSettings.AllowEnlarge, d.PreprocessSettings.AllowShrink, ScalingWidth = d.PreprocessSettings.Width, ScalingHeight = d.PreprocessSettings.Height, }); return Ok(devices); } /// /// 2. 新增设备 (写入配置并初始化) /// [HttpPost] public IActionResult Add([FromBody] CameraConfigDto dto) { if (_manager.GetDevice(dto.Id) != null) return Conflict($"设备ID {dto.Id} 已存在"); // DTO 转 Config (实际项目中建议用 AutoMapper) var config = MapToConfig(dto); _manager.AddDevice(config); // 添加到内存池 // 注意:此时 IsRunning 默认为 false,等待手动 Start 或 API 控制 return CreatedAtAction(nameof(GetAll), new { id = dto.Id }, dto); } /// /// 3. 编辑设备 (自动识别冷热更新) /// [HttpPut("{id}")] public async Task Update(long id, [FromBody] CameraConfigDto dto) { try { if (id != dto.Id) return BadRequest("ID 不匹配"); // 调用 Manager 的智能更新逻辑 (之前实现的 UpdateDeviceConfigAsync) await _manager.UpdateDeviceConfigAsync(id, MapToUpdateDto(dto)); return Ok(new { Success = true, Message = "配置已更新" }); } catch (KeyNotFoundException) { return NotFound(); } catch (System.Exception ex) { return StatusCode(500, ex.Message); } } /// /// 4. 删除设备 (销毁连接) /// [HttpDelete("{id}")] public async Task Remove(long id) { var device = _manager.GetDevice(id); if (device == null) return NotFound(); await device.StopAsync(); // 停流 _manager.RemoveDevice(id); // 从池中移除 return Ok($"设备 {id} 已移除"); } // ========================================================================== // 区域 B: 多进程流控订阅 (Subscription Strategy) // ========================================================================== // ========================================================================== // 区域 C: 句柄动态绑定 (Handle Binding) // ========================================================================== /// /// 6. 绑定显示窗口 (对应 A进程-句柄场景) /// [HttpPost("{id}/bind-handle")] public IActionResult BindHandle(long id, [FromBody] BindHandleDto dto) { var device = _manager.GetDevice(id); if (device == null) return NotFound(); // 构造动态选项,应用句柄 var options = new DynamicStreamOptions { RenderHandle = (System.IntPtr)dto.Handle }; device.ApplyOptions(options); // 触发驱动层的 OnApplyOptions device.AddAuditLog($"绑定新句柄: {dto.Handle} ({dto.Purpose})"); return Ok(new { Success = true }); } // ========================================================================== // 区域 D: 设备运行控制 // ========================================================================== /// /// 手动控制设备运行状态 (开关机) /// [HttpPost("{id}/power")] public async Task TogglePower(long id, [FromQuery] bool enabled) { var device = _manager.GetDevice(id); if (device == null) return NotFound(); // 1. 更新运行意图 device.IsRunning = enabled; // 2. 审计与执行 if (enabled) { device.AddAuditLog("用户指令:手动开启设备"); // 异步启动,Coordinator 也会在下个周期辅助检查 _ = device.StartAsync(); } else { device.AddAuditLog("用户指令:手动关闭设备"); await device.StopAsync(); } return Ok(new { DeviceId = id, IsRunning = enabled }); } /// /// 热应用动态参数 (如切换码流) /// [HttpPost("{id}/options")] public IActionResult ApplyOptions(long id, [FromBody] DynamicStreamOptions options) { var device = _manager.GetDevice(id); if (device == null) return NotFound(); // 1. 如果涉及码流切换,先同步更新配置对象 if (options.StreamType.HasValue) { var newConfig = device.Config.DeepCopy(); newConfig.StreamType = options.StreamType.Value; device.UpdateConfig(newConfig); } // 2. 应用到驱动层(触发热切换逻辑) device.ApplyOptions(options); return Ok(new { Message = "动态参数已发送", DeviceId = id }); } // ========================================================================== // 辅助方法 (Mapping) // ========================================================================== private VideoSourceConfig MapToConfig(CameraConfigDto dto) { return new VideoSourceConfig { Id = dto.Id, Name = dto.Name, Brand = (DeviceBrand)dto.Brand, IpAddress = dto.IpAddress, Port = dto.Port, Username = dto.Username, Password = dto.Password, ChannelIndex = dto.ChannelIndex, StreamType = dto.StreamType, RtspPath = dto.RtspPath, MainboardPort = dto.MainboardPort, MainboardIp = dto.MainboardIp, RenderHandle =dto.RenderHandle, }; } /// /// [辅助方法] 将全量配置 DTO 转换为更新 DTO /// private DeviceUpdateDto MapToUpdateDto(CameraConfigDto dto) { return new DeviceUpdateDto { // ========================================== // 1. 冷更新参数 (基础连接信息) // ========================================== IpAddress = dto.IpAddress, Port = dto.Port, Username = dto.Username, Password = dto.Password, ChannelIndex = dto.ChannelIndex, Brand = dto.Brand, RtspPath = dto.RtspPath, // ========================================== // 2. 热更新参数 (运行时属性) // ========================================== Name = dto.Name, Location = dto.Location, StreamType = dto.StreamType, MainboardIp = dto.MainboardIp, MainboardPort = dto.MainboardPort, RenderHandle = dto.RenderHandle, // 注意:通常句柄是通过 bind-handle 接口单独绑定的, // 但如果 ConfigDto 里包含了上次保存的句柄,也可以映射 // RenderHandle = dto.RenderHandle, // ========================================== // 3. 图像处理参数 // ========================================== AllowCompress = dto.AllowCompress, AllowExpand = dto.AllowExpand, TargetResolution = dto.TargetResolution, EnhanceImage = dto.EnhanceImage, UseGrayscale = dto.UseGrayscale }; } private readonly DisplayWindowManager _displayManager; // [新增] /// /// 综合订阅策略更新接口 /// 支持:本地 OpenCV 窗口、海康句柄穿透、本地录像、网络传输 /// [HttpPost("{id}/subscriptions")] public IActionResult UpdateSubscription(int id, [FromBody] SubscriptionDto dto) { var device = _manager.GetDevice(id); if (device == null) return NotFound("设备不存在"); // 1. 自动生成 ID 逻辑 if (string.IsNullOrWhiteSpace(dto.AppId)) { dto.AppId = $"SUB_{Guid.NewGuid().ToString("N").Substring(0, 8).ToUpper()}"; } // 2. 获取流控控制器 var controller = device.Controller; if (controller == null) return BadRequest("该设备类型不支持流控调度"); // 3. 处理注销逻辑 (FPS 为 0 代表停止订阅) if (dto.DisplayFps <= 0) { controller.Unregister(dto.AppId); // 停止显示管理器中所有相关的显示任务 (无论是本地窗口还是句柄绑定) _displayManager.StopDisplay(dto.AppId); device.AddAuditLog($"注销订阅: {dto.AppId}"); return Ok(new { Message = "Subscription removed", AppId = dto.AppId }); } // 4. 业务参数合法性校验 switch (dto.Type) { case SubscriptionType.LocalRecord when string.IsNullOrEmpty(dto.SavePath): return BadRequest("录像模式必须指定存放路径"); case SubscriptionType.HandleDisplay when string.IsNullOrEmpty(dto.Handle): return BadRequest("句柄显示模式必须提供窗口句柄"); } // 5. 将需求注册到流控控制器 controller.Register(new FrameRequirement(dto)); // 6. 路由显示逻辑 (核心整合点) if (dto.Type == SubscriptionType.LocalWindow) { // --- 保留旧版功能:启动本地 OpenCV 渲染窗口 --- _displayManager.StartDisplay(dto.AppId, id); } else if (dto.Type == SubscriptionType.HandleDisplay && !string.IsNullOrEmpty(dto.Handle)) { } device.AddAuditLog($"更新订阅: {dto.AppId} ({dto.Type}), 目标 {dto.DisplayFps} FPS"); return Ok(new { Success = true, AppId = dto.AppId, Message = "订阅策略已应用", CurrentConfig = controller.GetCurrentRequirements() // 返回当前所有订阅状态供前端同步 }); } [HttpGet("{id}/subscriptions")] public IActionResult GetSubscriptions(long id) { // 1. 检查设备是否存在 var camera = _manager.GetDevice(id); if (camera == null) { return NotFound(new { error = $"Camera {id} not found." }); } // 2. 从设备的 FrameController 获取当前活跃的订阅 // 注意:FrameController.GetCurrentRequirements() 返回的是 List // 它可以直接被序列化为 JSON var subs = camera.Controller.GetCurrentRequirements(); return Ok(subs); } // ============================================================= // 6. 新增:彻底删除/注销订阅 (RESTful DELETE) // URL: DELETE /api/cameras/{id}/subscriptions/{appId} // ============================================================= [HttpDelete("{id}/subscriptions/{appId}")] public IActionResult RemoveSubscription(long id, string appId) { // 1. 检查设备是否存在 var device = _manager.GetDevice(id); if (device == null) { return NotFound(new { error = $"Camera {id} not found." }); } // 2. 获取流控控制器 var controller = device.Controller; if (controller == null) { // 如果设备本身没有控制器(比如离线或不支持),也算删除成功 return Ok(new { success = true, message = "Device has no controller, nothing to remove." }); } // 3. 执行注销逻辑 (核心) // 从 FrameController 的分发列表中移除 controller.Unregister(appId); // 4. 清理显示资源 (重要!) // 参考您 UpdateSubscription 中的逻辑,必须同时停止 DisplayManager,否则窗口关不掉 _displayManager.StopDisplay(appId); // 5. 记录审计日志 device.AddAuditLog($"用户指令:彻底注销订阅 [{appId}]"); return Ok(new { success = true, message = $"Subscription {appId} has been removed and display stopped." }); } // 1. 获取单个设备详情(用于编辑回填) [HttpGet("{id}")] public IActionResult GetDevice(int id) { var cam = _manager.GetDevice(id); if (cam == null) return NotFound(); return Ok(cam.Config); // 返回原始配置对象 } // 3. 清除特定设备的日志 [HttpDelete("{id}/logs")] public IActionResult ClearLogs(int id) { var cam = _manager.GetDevice(id); cam?.ClearAuditLogs(); return Ok(); } //// 4. 抓图诊断 //[HttpGet("{id}/capture")] //public async Task Capture(int id) //{ // var cam = _manager.GetDevice(id); // if (cam == null) return NotFound(); // var bytes = await cam.CaptureCurrentFrameAsync(); // return File(bytes, "image/jpeg"); //} // ============================================================= // 3. 新增:更新图像处理/分辨率参数的接口 // URL 示例: POST /api/cameras/1001/processing // ============================================================= [HttpPost("{id}/processing")] public IActionResult UpdateProcessingOptions(long id, [FromBody] ProcessingOptions options) { // A. 检查相机是否存在 var camera = _manager.GetDevice(id); if (camera == null) { return NotFound(new { error = $"Camera {id} not found." }); } // B. 参数校验 (防止宽高为0导致报错) if (options.TargetWidth <= 0 || options.TargetHeight <= 0) { return BadRequest(new { error = "Target dimensions must be greater than 0." }); } // C. 执行更新 (热更,立即生效) // ScaleWorker 下一帧处理时会自动读取这个新配置 _configManager.UpdateOptions(id, options); return Ok(new { success = true, message = "Image processing options updated.", currentConfig = options }); } // 在 CamerasController 类中添加 // ============================================================= // 新增:获取/回显图像处理参数的接口 // URL: GET /api/cameras/{id}/processing // ============================================================= [HttpGet("{id}/processing")] public IActionResult GetProcessingOptions(long id) { // 1. 检查相机是否存在 var camera = _manager.GetDevice(id); if (camera == null) { return NotFound(new { error = $"Camera {id} not found." }); } // 2. 从配置管理器中获取当前配置 // 注意:ProcessingConfigManager 内部应该处理好逻辑: // 如果该设备还没配过,它会自动返回 new ProcessingOptions() (默认值) var options = _configManager.GetOptions(id); // 3. 返回 JSON 给前端 return Ok(options); } /// /// 获取设备时间 (支持海康/大华等具备此能力的设备) /// [HttpGet("{id}/time")] public async Task GetDeviceTime(long id) { var device = _manager.GetDevice(id); if (device == null) return NotFound(new { error = "Device not found" }); if (!device.IsPhysicalOnline) return BadRequest(new { error = "Device is offline" }); // 【核心】模式匹配:判断这个设备是否实现了“时间同步接口” if (device is ITimeSyncFeature timeFeature) { try { var time = await timeFeature.GetTimeAsync(); return Ok(new { deviceId = id, currentTime = time }); } catch (Exception ex) { return StatusCode(500, new { error = ex.Message }); } } // 如果是 RTSP/USB 等不支持的设备 return BadRequest(new { error = "This device does not support time synchronization." }); } /// /// 设置设备时间 /// [HttpPost("{id}/time")] public async Task SetDeviceTime(long id, [FromBody] DateTime time) { var device = _manager.GetDevice(id); if (device == null) return NotFound(); if (device is ITimeSyncFeature timeFeature) { try { await timeFeature.SetTimeAsync(time); return Ok(new { success = true, message = $"Time synced to {time}" }); } catch (Exception ex) { return StatusCode(500, new { error = ex.Message }); } } return BadRequest(new { error = "This device does not support time synchronization." }); } /// /// 远程重启设备 /// [HttpPost("{id}/reboot")] public async Task RebootDevice(long id) { var device = _manager.GetDevice(id); if (device == null) return NotFound(new { error = "Device not found" }); // 依然是两道防线:先检查在线,再检查能力 if (!device.IsOnline) return BadRequest(new { error = "Device is offline" }); if (device is IRebootFeature rebootFeature) { try { await rebootFeature.RebootAsync(); // 记录审计日志 (建议加上) device.AddAuditLog("用户执行了远程重启"); return Ok(new { success = true, message = "重启指令已发送,设备将在几分钟后重新上线。" }); } catch (Exception ex) { return StatusCode(500, new { error = ex.Message }); } } return BadRequest(new { error = "This device does not support remote reboot." }); } [HttpPost("{id}/ptz")] public async Task PtzControl(long id, [FromBody] PtzControlDto dto) { var device = _manager.GetDevice(id); if (device == null) return NotFound(); if (!device.IsOnline) return BadRequest("Device offline"); if (device is IPtzFeature ptz) { try { // 逻辑分流 if (dto.Duration > 0) { // 场景:点动模式 (一次调用,自动停止) // 建议限制一下最大时长,防止前端传个 10000秒 导致云台转疯了 int safeDuration = Math.Clamp(dto.Duration, 50, 2000); await ptz.PtzStepAsync(dto.Action, safeDuration, dto.Speed); } else { // 场景:手动模式 (按下/松开) await ptz.PtzControlAsync(dto.Action, dto.Stop, dto.Speed); } return Ok(); } catch (Exception ex) { return StatusCode(500, new { error = ex.Message }); }// } return BadRequest("Device does not support PTZ."); } }