摄像头播放后,增加分辨率的显示
This commit is contained in:
@@ -293,51 +293,40 @@ public class CamerasController : ControllerBase
|
||||
return BadRequest("Device implies no controller");
|
||||
}
|
||||
|
||||
// 1. 获取单个设备详情(用于编辑回填)
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult GetDevice(int id)
|
||||
{
|
||||
var cam = _manager.GetDevice(id);
|
||||
if (cam == null) return NotFound();
|
||||
return Ok(cam.Config); // 返回原始配置对象
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 5. 注册/更新进程的流需求 (A/B/C/D 场景核心)
|
||||
///// </summary>
|
||||
///// <remarks>
|
||||
///// 示例场景:
|
||||
///// - 主进程配置(B): { "appId": "Main_Config", "displayFps": 25, "analysisFps": 0 }
|
||||
///// - AI进程(C): { "appId": "AI_Core", "displayFps": 0, "analysisFps": 5 }
|
||||
///// </remarks>
|
||||
//[HttpPost("{id}/subscriptions")]
|
||||
//public IActionResult UpdateSubscription(long id, [FromBody] SubscriptionDto sub)
|
||||
// 2. 更新设备(保存功能)
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> UpdateDevice(int id, [FromBody] VideoSourceConfig config)
|
||||
{
|
||||
// 核心逻辑:先停止旧设备 -> 更新配置 -> 重新添加到容器 -> 如果之前在运行则重新启动
|
||||
await _manager.UpdateDeviceAsync(id, config);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// 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 device = _manager.GetDevice(id);
|
||||
// if (device == null) return NotFound();
|
||||
|
||||
// // 逻辑转换:将 "显示帧" 和 "分析帧" 映射到底层控制器的注册表
|
||||
|
||||
// // 1. 处理显示需求
|
||||
// string displayKey = $"{sub.AppId}_Display";
|
||||
// if (sub.DisplayFps > 0)
|
||||
// {
|
||||
// // 告诉控制器:这个 App 需要 X 帧用于显示
|
||||
// device.Controller.Register(displayKey, sub.DisplayFps);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // 如果不需要,移除注册
|
||||
// device.Controller.Unregister(displayKey);
|
||||
// }
|
||||
|
||||
// // 2. 处理分析需求
|
||||
// string analysisKey = $"{sub.AppId}_Analysis";
|
||||
// if (sub.AnalysisFps > 0)
|
||||
// {
|
||||
// // 告诉控制器:这个 App 需要 Y 帧用于分析
|
||||
// device.Controller.Register(analysisKey, sub.AnalysisFps);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// device.Controller.Unregister(analysisKey);
|
||||
// }
|
||||
|
||||
// // 运维审计
|
||||
// device.AddAuditLog($"更新订阅策略 [{sub.AppId}]: Display={sub.DisplayFps}, Analysis={sub.AnalysisFps}");
|
||||
|
||||
// return Ok(new { Message = "订阅策略已更新", DeviceId = id });
|
||||
// var cam = _manager.GetDevice(id);
|
||||
// if (cam == null) return NotFound();
|
||||
// var bytes = await cam.CaptureCurrentFrameAsync();
|
||||
// return File(bytes, "image/jpeg");
|
||||
//}
|
||||
}
|
||||
@@ -110,46 +110,68 @@ public class FrameConsumer : IDisposable
|
||||
/// </summary>
|
||||
private void RenderLoop()
|
||||
{
|
||||
// 创建 OpenCV 显示窗口
|
||||
Cv2.NamedWindow(_windowName, WindowFlags.Normal);
|
||||
bool isWindowCreated = false;
|
||||
|
||||
try
|
||||
{
|
||||
// 阻塞遍历队列:队列空时等待,收到取消信号时退出
|
||||
foreach (var frame in _frameBuffer.GetConsumingEnumerable(_cts.Token))
|
||||
// 我们不再使用简单的 foreach 阻塞等待数据,
|
||||
// 而是改用非阻塞模式或带有超时的读取,以保证 WaitKey 的活性
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
// 尝试在 30ms 内获取一帧数据(相当于 33 fps 的响应速度)
|
||||
if (_frameBuffer.TryTake(out var frame, 30, _cts.Token))
|
||||
{
|
||||
// 渲染有效性校验:Mat 未释放且不为空
|
||||
if (frame.InternalMat != null && !frame.InternalMat.IsDisposed)
|
||||
try
|
||||
{
|
||||
// 零拷贝渲染:直接引用 InternalMat,无内存复制开销
|
||||
Cv2.ImShow(_windowName, frame.InternalMat);
|
||||
// 1ms 等待 UI 事件响应(必须调用,否则窗口无响应)
|
||||
Cv2.WaitKey(1);
|
||||
if (frame.InternalMat != null && !frame.InternalMat.IsDisposed)
|
||||
{
|
||||
if (!isWindowCreated)
|
||||
{
|
||||
Cv2.NamedWindow(_windowName, WindowFlags.Normal);
|
||||
isWindowCreated = true;
|
||||
}
|
||||
|
||||
Cv2.ImShow(_windowName, frame.InternalMat);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
frame.Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
// 【核心修复】无论有没有取到帧,都要执行 WaitKey
|
||||
// 只有这样,窗口在没视频时才能被拖动、最小化或手动点击 X 关闭
|
||||
if (isWindowCreated)
|
||||
{
|
||||
Debug.WriteLine($"[RenderError] 窗口[{_windowName}]渲染失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 至关重要:渲染完成后释放帧引用
|
||||
// 引用计数归零 → 帧自动回池复用,避免内存泄漏
|
||||
frame.Dispose();
|
||||
// 1ms 的等待足以处理 Windows 窗口消息
|
||||
int key = Cv2.WaitKey(1);
|
||||
|
||||
// 如果用户点击了 OpenCV 窗口右上角的 X (部分版本支持)
|
||||
// 或者按下 ESC,可以根据需要在这里处理
|
||||
if (key == 27) break;
|
||||
|
||||
// 检查窗口是否还存在(防止用户手动关掉窗口后报错)
|
||||
// 如果窗口被手动关闭,我们标记为未创建,下次有流时重新弹窗
|
||||
try
|
||||
{
|
||||
if (Cv2.GetWindowProperty(_windowName, WindowPropertyFlags.Visible) < 1)
|
||||
{
|
||||
isWindowCreated = false;
|
||||
}
|
||||
}
|
||||
catch { isWindowCreated = false; }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消,无需处理
|
||||
Debug.WriteLine($"[RenderInfo] 窗口[{_windowName}]渲染循环已取消");
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
// 销毁 OpenCV 窗口,释放 UI 资源
|
||||
Cv2.DestroyWindow(_windowName);
|
||||
if (isWindowCreated)
|
||||
{
|
||||
Cv2.DestroyWindow(_windowName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,28 +39,39 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
/// <param name="config">相机设备配置信息</param>
|
||||
public void AddDevice(VideoSourceConfig config)
|
||||
{
|
||||
// [安全防护] 销毁过程中禁止添加新设备
|
||||
if (_isDisposed) return;
|
||||
// 防止重复添加同一设备
|
||||
if (_cameraPool.ContainsKey(config.Id)) return;
|
||||
//// [安全防护] 销毁过程中禁止添加新设备
|
||||
//if (_isDisposed) return;
|
||||
//// 防止重复添加同一设备
|
||||
//if (_cameraPool.ContainsKey(config.Id)) return;
|
||||
|
||||
// 1. 根据设备品牌实例化对应的驱动实现类
|
||||
BaseVideoSource source = config.Brand switch
|
||||
{
|
||||
DeviceBrand.HikVision => new HikVideoSource(config),
|
||||
_ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}")
|
||||
};
|
||||
//// 1. 根据设备品牌实例化对应的驱动实现类
|
||||
//BaseVideoSource source = config.Brand switch
|
||||
//{
|
||||
// DeviceBrand.HikVision => new HikVideoSource(config),
|
||||
// _ => throw new NotSupportedException($"不支持的相机品牌: {config.Brand}")
|
||||
//};
|
||||
|
||||
// 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
|
||||
if (_isEngineStarted)
|
||||
{
|
||||
source.IsRunning = true;
|
||||
}
|
||||
//// 2. [Fix Bug A] 动态激活逻辑:引擎已启动时,新设备直接标记为运行状态
|
||||
//if (_isEngineStarted)
|
||||
//{
|
||||
// source.IsRunning = true;
|
||||
//}
|
||||
|
||||
// 3. 将设备注册到内存池与协调器,纳入统一管理
|
||||
if (_cameraPool.TryAdd(config.Id, source))
|
||||
//// 3. 将设备注册到内存池与协调器,纳入统一管理
|
||||
//if (_cameraPool.TryAdd(config.Id, source))
|
||||
//{
|
||||
// _coordinator.Register(source);
|
||||
//}
|
||||
|
||||
// 使用工厂方法创建
|
||||
var device = CreateDeviceInstance(config);
|
||||
|
||||
if (!_cameraPool.TryAdd(config.Id, device))
|
||||
{
|
||||
_coordinator.Register(source);
|
||||
// 如果添加失败(ID冲突),由于 device 还没被使用,直接释放掉
|
||||
// 这里不需要 await,因为刚 new 出来的对象还没 connect
|
||||
device.DisposeAsync().AsTask().Wait();
|
||||
throw new InvalidOperationException($"设备 ID {config.Id} 已存在");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +212,10 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
TotalFrames = cam.TotalFrames,
|
||||
HealthScore = healthScore,
|
||||
LastErrorMessage = cam.Status == VideoSourceStatus.Faulted ? "设备故障或网络中断" : null,
|
||||
Timestamp = System.DateTime.Now
|
||||
Timestamp = System.DateTime.Now,
|
||||
// [新增] 映射分辨率
|
||||
Width = cam.Width,
|
||||
Height = cam.Height,
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
@@ -342,4 +356,75 @@ public class CameraManager : IDisposable, IAsyncDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 更新设备配置(热重载)
|
||||
/// 流程:停止旧设备 -> 释放资源 -> 创建新设备 -> 替换引用 -> (可选)自动重启
|
||||
/// </summary>
|
||||
public async Task UpdateDeviceAsync(int id, VideoSourceConfig newConfig)
|
||||
{
|
||||
// 1. 检查设备是否存在
|
||||
if (!_cameraPool.TryGetValue(id, out var oldDevice))
|
||||
{
|
||||
throw new KeyNotFoundException($"设备 #{id} 不存在");
|
||||
}
|
||||
|
||||
// 2. 捕获旧状态(用于决定是否需要自动重启新设备)
|
||||
// 如果旧设备正在运行或尝试连接中,我们在更新后应该尝试恢复它
|
||||
bool wasRunning = oldDevice.IsRunning ||
|
||||
oldDevice.Status == VideoSourceStatus.Playing ||
|
||||
oldDevice.Status == VideoSourceStatus.Connecting;
|
||||
|
||||
// 获取旧的流控需求(如果希望更新配置后,之前的订阅依然生效,需要把需求搬过去)
|
||||
// 这里简化处理:更新配置通常意味着环境变了,我们选择清空旧订阅,让前端重新下发,或者你可以手动拷贝
|
||||
// var oldRequirements = oldDevice.Controller.GetCurrentRequirements();
|
||||
|
||||
Console.WriteLine($"[Manager] 正在更新设备 #{id},配置变更中...");
|
||||
|
||||
// 3. 【关键步骤】优雅停止并销毁旧实例
|
||||
// 必须先 Stop 再 Dispose,确保 SDK 句柄(如 lUserId, lRealPlayHandle)被释放
|
||||
try
|
||||
{
|
||||
await oldDevice.StopAsync(); // 停止取流
|
||||
await oldDevice.DisposeAsync(); // 注销登录、释放非托管资源
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Manager] 销毁旧设备时警告: {ex.Message}");
|
||||
// 继续执行,不要因为旧设备销毁失败阻断新配置的应用
|
||||
}
|
||||
|
||||
// 4. 使用新配置创建新实例
|
||||
// 建议将创建逻辑提取为私有方法 CreateDeviceInstance,避免与 AddDevice 代码重复
|
||||
var newDevice = CreateDeviceInstance(newConfig);
|
||||
|
||||
// 5. 原子替换 (ConcurrentDictionary 的索引器赋值是线程安全的)
|
||||
_cameraPool[id] = newDevice;
|
||||
|
||||
Console.WriteLine($"[Manager] 设备 #{id} 实例已重建。");
|
||||
|
||||
// 6. 状态恢复
|
||||
if (wasRunning)
|
||||
{
|
||||
Console.WriteLine($"[Manager] 检测到设备 #{id} 之前为运行状态,正在自动重启...");
|
||||
// 不等待 StartAsync 完成,避免阻塞 HTTP 请求太久
|
||||
// 如果希望前端看到转圈直到启动完成,则加上 await
|
||||
await newDevice.StartAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [辅助工厂方法] 根据配置创建具体的驱动实例
|
||||
/// 请确保你的 AddDevice 方法也改为调用此方法,减少重复代码
|
||||
/// </summary>
|
||||
private BaseVideoSource CreateDeviceInstance(VideoSourceConfig config)
|
||||
{
|
||||
return config.Brand switch
|
||||
{
|
||||
DeviceBrand.HikVision => new HikVideoSource(config),
|
||||
// 如果你有大华或其他品牌,在这里扩展
|
||||
// DeviceBrand.Dahua => new DahuaVideoSource(config),
|
||||
_ => throw new NotSupportedException($"不支持的设备品牌: {config.Brand}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,12 @@ public class CameraTelemetryInfo
|
||||
/// <summary> 实时码率 (Mbps) </summary>
|
||||
public double Bitrate { get; set; }
|
||||
|
||||
// [新增] 视频宽度
|
||||
public int Width { get; set; }
|
||||
|
||||
// [新增] 视频高度
|
||||
public int Height { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region --- 健康度与统计 (Health & Statistics) ---
|
||||
|
||||
@@ -683,4 +683,14 @@ public abstract class BaseVideoSource : IVideoSource, IAsyncDisposable, IDeviceC
|
||||
public FrameController Controller { get; protected set; }
|
||||
|
||||
#endregion
|
||||
|
||||
// 自动从 SmartFrame 中提取
|
||||
public int Width { get; protected set; }
|
||||
public int Height { get; protected set; }
|
||||
|
||||
public void ClearAuditLogs()
|
||||
{
|
||||
_auditLogs.Clear();
|
||||
AddAuditLog("用户清空了审计日志");
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,17 @@ public class HikVideoSource : BaseVideoSource
|
||||
|
||||
private void SafeOnDecodingCallBack(int nPort, IntPtr pBuf, int nSize, ref HikPlayMethods.FRAME_INFO pFrameInfo, int nReserved1, int nReserved2)
|
||||
{
|
||||
// [优化] 维持心跳,防止被哨兵误杀
|
||||
MarkFrameReceived(0);
|
||||
|
||||
// [新增] 捕获并更新分辨率
|
||||
// 只有当分辨率发生变化时才写入,减少属性赋值开销
|
||||
if (Width != pFrameInfo.nWidth || Height != pFrameInfo.nHeight)
|
||||
{
|
||||
Width = pFrameInfo.nWidth;
|
||||
Height = pFrameInfo.nHeight;
|
||||
}
|
||||
|
||||
// 1. [核心流控] 询问基类控制器:这帧要不要?
|
||||
// 之前失效是因为操作的是子类被遮蔽的 Controller,现在复用基类 Controller,逻辑就通了。
|
||||
// 传入真实的输入帧率作为参考基准
|
||||
@@ -303,9 +314,6 @@ public class HikVideoSource : BaseVideoSource
|
||||
// 如果没人要,直接丢弃,不进行 Mat 转换,节省 CPU
|
||||
if (!decision.IsCaptured) return;
|
||||
|
||||
// [优化] 维持心跳,防止被哨兵误杀
|
||||
MarkFrameReceived(0);
|
||||
|
||||
int width = pFrameInfo.nWidth;
|
||||
int height = pFrameInfo.nHeight;
|
||||
|
||||
@@ -348,7 +356,8 @@ public class HikVideoSource : BaseVideoSource
|
||||
}
|
||||
finally
|
||||
{
|
||||
smartFrame.Dispose();
|
||||
if (smartFrame != null)
|
||||
smartFrame.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
SHH.CameraSdk/Temp/UserActionFilter.cs
Normal file
18
SHH.CameraSdk/Temp/UserActionFilter.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
public class UserActionFilter : IActionFilter
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
if (context.HttpContext.Request.Method != "GET")
|
||||
{
|
||||
var user = context.HttpContext.Connection.RemoteIpAddress;
|
||||
var path = context.HttpContext.Request.Path;
|
||||
var method = context.HttpContext.Request.Method;
|
||||
|
||||
// 写入全局操作日志文件或数据库
|
||||
File.AppendAllText("user_actions.log", $"{DateTime.Now} | {user} | {method} {path}\n");
|
||||
}
|
||||
}
|
||||
public void OnActionExecuting(ActionExecutingContext context) { }
|
||||
}
|
||||
Reference in New Issue
Block a user