Files
Ayay/SHH.CameraSdk/Controllers/CamerasController.cs
2025-12-31 20:43:54 +08:00

536 lines
18 KiB
C#
Raw 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 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)
// ==========================================================================
/// <summary>
/// 1. 获取所有设备清单
/// </summary>
[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);
}
/// <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,
MainboardPort = dto.MainboardPort,
MainboardIp = dto.MainboardIp,
RenderHandle =dto.RenderHandle,
};
}
/// <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,
// ==========================================
// 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; // [新增]
/// <summary>
/// 综合订阅策略更新接口
/// 支持:本地 OpenCV 窗口、海康句柄穿透、本地录像、网络传输
/// </summary>
[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() // 返回当前所有订阅状态供前端同步
});
}
// 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<IActionResult> 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);
}
/// <summary>
/// 获取设备时间 (支持海康/大华等具备此能力的设备)
/// </summary>
[HttpGet("{id}/time")]
public async Task<IActionResult> 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." });
}
/// <summary>
/// 设置设备时间
/// </summary>
[HttpPost("{id}/time")]
public async Task<IActionResult> 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." });
}
/// <summary>
/// 远程重启设备
/// </summary>
[HttpPost("{id}/reboot")]
public async Task<IActionResult> 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<IActionResult> 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.");
}
}