摄像头播放后,增加分辨率的显示

This commit is contained in:
2025-12-26 18:55:04 +08:00
parent e98059fd30
commit 108c97924f
7 changed files with 232 additions and 93 deletions

View File

@@ -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);
}
}
}

View File

@@ -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}")
};
}
}

View File

@@ -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) ---