2025-12-26 12:15:10 +08:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SHH.CameraSdk;
|
|
|
|
|
|
|
|
|
|
|
|
[ApiController]
|
|
|
|
|
|
[Route("api/[controller]")]
|
|
|
|
|
|
public class CamerasController : ControllerBase
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly CameraManager _manager;
|
|
|
|
|
|
|
2025-12-26 17:28:07 +08:00
|
|
|
|
// 构造函数注入管理器
|
|
|
|
|
|
public CamerasController(CameraManager manager, DisplayWindowManager displayManager)
|
2025-12-26 12:15:10 +08:00
|
|
|
|
{
|
|
|
|
|
|
_manager = manager;
|
2025-12-26 17:28:07 +08:00
|
|
|
|
_displayManager = displayManager;
|
2025-12-26 12:15:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==========================================================================
|
|
|
|
|
|
// 区域 A: 设备全生命周期管理 (CRUD)
|
|
|
|
|
|
// ==========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 1. 获取所有设备清单
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public IActionResult GetAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
var devices = _manager.GetAllDevices().Select(d => new
|
|
|
|
|
|
{
|
|
|
|
|
|
d.Id,
|
|
|
|
|
|
d.Config.IpAddress,
|
|
|
|
|
|
d.Config.Name,
|
|
|
|
|
|
Status = d.Status.ToString(),
|
|
|
|
|
|
d.RealFps,
|
|
|
|
|
|
d.TotalFrames
|
|
|
|
|
|
});
|
|
|
|
|
|
return Ok(devices);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 2. 新增设备 (写入配置并初始化)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 3. 编辑设备 (自动识别冷热更新)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPut("{id}")]
|
|
|
|
|
|
public async Task<IActionResult> 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); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 4. 删除设备 (销毁连接)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpDelete("{id}")]
|
|
|
|
|
|
public async Task<IActionResult> 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)
|
|
|
|
|
|
// ==========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 6. 绑定显示窗口 (对应 A进程-句柄场景)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[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: 设备运行控制
|
|
|
|
|
|
// ==========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 手动控制设备运行状态 (开关机)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost("{id}/power")]
|
|
|
|
|
|
public async Task<IActionResult> 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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 热应用动态参数 (如切换码流)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// [辅助方法] 将全量配置 DTO 转换为更新 DTO
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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,
|
|
|
|
|
|
MainboardIp = dto.MainboardIp,
|
|
|
|
|
|
MainboardPort = dto.MainboardPort,
|
|
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
// 2. 热更新参数 (运行时属性)
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
Name = dto.Name,
|
|
|
|
|
|
Location = dto.Location,
|
|
|
|
|
|
StreamType = dto.StreamType,
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:通常句柄是通过 bind-handle 接口单独绑定的,
|
|
|
|
|
|
// 但如果 ConfigDto 里包含了上次保存的句柄,也可以映射
|
|
|
|
|
|
// RenderHandle = dto.RenderHandle,
|
|
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
// 3. 图像处理参数
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
|
AllowCompress = dto.AllowCompress,
|
|
|
|
|
|
AllowExpand = dto.AllowExpand,
|
|
|
|
|
|
TargetResolution = dto.TargetResolution,
|
|
|
|
|
|
EnhanceImage = dto.EnhanceImage,
|
|
|
|
|
|
UseGrayscale = dto.UseGrayscale
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-12-26 13:11:58 +08:00
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
|
|
|
|
|
|
private readonly DisplayWindowManager _displayManager; // [新增]
|
2025-12-26 13:11:58 +08:00
|
|
|
|
/// <summary>
|
2025-12-27 14:16:50 +08:00
|
|
|
|
/// 综合订阅策略更新接口
|
|
|
|
|
|
/// 支持:本地 OpenCV 窗口、海康句柄穿透、本地录像、网络传输
|
2025-12-26 13:11:58 +08:00
|
|
|
|
/// </summary>
|
2025-12-27 14:16:50 +08:00
|
|
|
|
[HttpPost("{id}/subscriptions")]
|
|
|
|
|
|
public IActionResult UpdateSubscription(int id, [FromBody] SubscriptionDto dto)
|
2025-12-26 13:11:58 +08:00
|
|
|
|
{
|
|
|
|
|
|
var device = _manager.GetDevice(id);
|
2025-12-27 14:16:50 +08:00
|
|
|
|
if (device == null) return NotFound("设备不存在");
|
2025-12-26 13:11:58 +08:00
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
// 1. 自动生成 ID 逻辑
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(dto.AppId))
|
|
|
|
|
|
{
|
|
|
|
|
|
dto.AppId = $"SUB_{Guid.NewGuid().ToString("N").Substring(0, 8).ToUpper()}";
|
|
|
|
|
|
}
|
2025-12-26 13:11:58 +08:00
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
// 2. 获取流控控制器
|
|
|
|
|
|
var controller = device.Controller;
|
|
|
|
|
|
if (controller == null) return BadRequest("该设备类型不支持流控调度");
|
2025-12-26 17:28:07 +08:00
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
// 3. 处理注销逻辑 (FPS 为 0 代表停止订阅)
|
|
|
|
|
|
if (dto.DisplayFps <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
controller.Unregister(dto.AppId);
|
2025-12-26 17:28:07 +08:00
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
// 停止显示管理器中所有相关的显示任务 (无论是本地窗口还是句柄绑定)
|
|
|
|
|
|
_displayManager.StopDisplay(dto.AppId);
|
2025-12-26 17:28:07 +08:00
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
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(dto.AppId, dto.DisplayFps);
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 路由显示逻辑 (核心整合点)
|
|
|
|
|
|
if (dto.Type == SubscriptionType.LocalWindow)
|
|
|
|
|
|
{
|
|
|
|
|
|
// --- 保留旧版功能:启动本地 OpenCV 渲染窗口 ---
|
|
|
|
|
|
_displayManager.StartDisplay(dto.AppId, id);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (dto.Type == SubscriptionType.HandleDisplay && !string.IsNullOrEmpty(dto.Handle))
|
2025-12-26 17:28:07 +08:00
|
|
|
|
{
|
2025-12-27 14:16:50 +08:00
|
|
|
|
|
2025-12-26 17:28:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 14:16:50 +08:00
|
|
|
|
device.AddAuditLog($"更新订阅: {dto.AppId} ({dto.Type}), 目标 {dto.DisplayFps} FPS");
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Success = true,
|
|
|
|
|
|
AppId = dto.AppId,
|
|
|
|
|
|
Message = "订阅策略已应用",
|
|
|
|
|
|
CurrentConfig = controller.GetCurrentRequirements() // 返回当前所有订阅状态供前端同步
|
|
|
|
|
|
});
|
2025-12-26 17:28:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 18:55:04 +08:00
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
2025-12-26 17:28:07 +08:00
|
|
|
|
|
2025-12-26 18:55:04 +08:00
|
|
|
|
//// 4. 抓图诊断
|
|
|
|
|
|
//[HttpGet("{id}/capture")]
|
|
|
|
|
|
//public async Task<IActionResult> Capture(int id)
|
2025-12-26 17:28:07 +08:00
|
|
|
|
//{
|
2025-12-26 18:55:04 +08:00
|
|
|
|
// var cam = _manager.GetDevice(id);
|
|
|
|
|
|
// if (cam == null) return NotFound();
|
|
|
|
|
|
// var bytes = await cam.CaptureCurrentFrameAsync();
|
|
|
|
|
|
// return File(bytes, "image/jpeg");
|
2025-12-26 17:28:07 +08:00
|
|
|
|
//}
|
2025-12-26 12:15:10 +08:00
|
|
|
|
}
|